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/README.md CHANGED
@@ -192,6 +192,64 @@ The report can include:
192
192
 
193
193
  These metrics are diagnostic rather than a score for the quality of your application.
194
194
 
195
+ ### Causal graph
196
+
197
+ `printBasisReport()` gives a diagnosis. If you want to see the evidence that report is based on, Basis also exposes the observed update graph directly.
198
+
199
+ `printBasisGraph()` does nothing unless `debug` is on (same as `printBasisReport()`). `getBasisGraph()` always returns the current snapshot, regardless of `debug`, since it's just serializing the graph, not printing anything.
200
+
201
+ ```js
202
+ window.printBasisGraph()
203
+ ```
204
+
205
+ prints it to the console, grouped by source:
206
+
207
+ ```
208
+ 📊 BASIS | CAUSAL GRAPH 7 nodes · 7 edges · 2 sources · buffer window 50
209
+ parent → child = observed cause → update. (×N) = times in this window.
210
+ ⚡ Event · 3 targets · ×2
211
+ BooleanEntanglement.tsx → isLoading redundant
212
+ BooleanEntanglement.tsx → isSuccess redundant
213
+ BooleanEntanglement.tsx → hasData redundant
214
+ ↯ WeatherLab.tsx → effect @ L7
215
+ WeatherLab.tsx → fahrenheit (×2)
216
+ ```
217
+
218
+ For the raw data, either from the console:
219
+
220
+ ```js
221
+ window.getBasisGraph()
222
+ ```
223
+
224
+ or imported directly, if you're building your own tooling on top of it rather than reading it from the console:
225
+
226
+ ```ts
227
+ import { getBasisGraph } from 'react-state-basis';
228
+ import type { BasisGraphJSON } from 'react-state-basis';
229
+ ```
230
+
231
+ Either way it returns:
232
+
233
+ ```ts
234
+ {
235
+ generatedAt: number;
236
+ bufferWindowSize: number;
237
+ eventTtlMs: number;
238
+ nodes: { id, name, file, role, density, redundant }[];
239
+ edges: { source, target, weight }[];
240
+ eventGroups: { sourceIds, occurrences, edges }[];
241
+ }
242
+ ```
243
+
244
+ A few things worth knowing about the shape:
245
+
246
+ * `role` distinguishes real state (`local` / `context` / `store` / `proj`) from `effect` sources, virtual `event` triggers, and `unknown` (a graph edge with no recognized shape - e.g. a custom integration recording edges directly). `effect`, `event`, and `unknown` all have `density: null` - none of them has a real update history of its own.
247
+ * `bufferWindowSize` and `eventTtlMs` are two different clocks: state nodes' `density` is measured over the last `bufferWindowSize` ticks, while virtual `event` nodes are pruned after `eventTtlMs` of inactivity. They're unrelated, so don't read one as describing the other.
248
+ * User interactions are recorded as virtual, per-frame `event` triggers. Repeated interactions that produce the exact same fan-out (same targets, same weights) are collapsed into one entry in `eventGroups`, so clicking the same button 20 times doesn't produce 20 near-identical entries. `nodes` and `edges` remain the full, ungrouped data if you need it - the node/edge counts in the header above are always raw counts, not the number of lines shown after grouping.
249
+ * Only nodes that appear on at least one edge are included - a registered variable that has never caused or received an update won't show up.
250
+
251
+ This is meant for inspecting what Basis actually observed, building your own tooling on top of it, or attaching to a bug report - not as a second diagnostic layer alongside `printBasisReport()`.
252
+
195
253
  ### Runtime metrics
196
254
 
197
255
  You can inspect engine metrics with:
@@ -327,4 +385,4 @@ For a deeper look at the implementation and underlying model:
327
385
 
328
386
  Built by [LP](https://github.com/liovic) • [MIT License](https://opensource.org/licenses/MIT)
329
387
 
330
- </div>
388
+ </div>
@@ -1,30 +1,89 @@
1
+ // src/core/constants.ts
2
+ var WINDOW_SIZE = 50;
3
+ var PAIR_RARITY_TARGET = 0.01;
4
+ var RELATIVE_OVERLAP_FLOOR = 0.65;
5
+ var LOOP_THRESHOLD = 150;
6
+ var VOLATILITY_THRESHOLD = 25;
7
+ var INSTANCE_SEP = "##";
8
+
1
9
  // src/core/math.ts
2
- var calculateSimilarityCircular = (bufferA, headA, bufferB, headB, offset) => {
10
+ var choose = (n, k) => {
11
+ if (k < 0 || k > n) return 0;
12
+ k = Math.min(k, n - k);
13
+ let result = 1;
14
+ for (let i = 0; i < k; i++) {
15
+ result = result * (n - i) / (i + 1);
16
+ }
17
+ return result;
18
+ };
19
+ var hypergeomPMF = (k, n1, m1, N) => {
20
+ const total = choose(N, n1);
21
+ if (total === 0) return 0;
22
+ return choose(m1, k) * choose(N - m1, n1 - k) / total;
23
+ };
24
+ var hypergeomUpperTailP = (kStart, n1, m1, N) => {
25
+ const kMax = Math.min(n1, m1);
26
+ let p = 0;
27
+ for (let k = Math.max(0, kStart); k <= kMax; k++) {
28
+ p += hypergeomPMF(k, n1, m1, N);
29
+ }
30
+ return p;
31
+ };
32
+ var minOverlapForRarity = (n1, m1, N, targetP) => {
33
+ const kMax = Math.min(n1, m1);
34
+ for (let k = 0; k <= kMax; k++) {
35
+ if (hypergeomUpperTailP(k, n1, m1, N) <= targetP) return k;
36
+ }
37
+ return kMax + 1;
38
+ };
39
+ var overlapThresholdCache = /* @__PURE__ */ new Map();
40
+ var getMinOverlap = (densityA, densityB, windowSize) => {
41
+ const lo = Math.min(densityA, densityB) | 0;
42
+ const hi = Math.max(densityA, densityB) | 0;
43
+ const key = `${lo}_${hi}_${windowSize}`;
44
+ let cached = overlapThresholdCache.get(key);
45
+ if (cached === void 0) {
46
+ const rare = minOverlapForRarity(lo, hi, windowSize, PAIR_RARITY_TARGET);
47
+ const floor = Math.ceil(RELATIVE_OVERLAP_FLOOR * lo);
48
+ cached = Math.max(rare, floor);
49
+ overlapThresholdCache.set(key, cached);
50
+ }
51
+ return cached;
52
+ };
53
+ var isSignificantOverlap = (overlap, densityA, densityB, windowSize) => {
54
+ if (densityA < 2 || densityB < 2) return false;
55
+ return overlap >= getMinOverlap(densityA, densityB, windowSize);
56
+ };
57
+ var countOverlapsCircular = (bufferA, headA, bufferB, headB) => {
3
58
  const L = bufferA.length;
4
- let dot = 0, magA = 0, magB = 0;
5
- const baseOffset = ((headB - headA + offset) % L + L) % L;
59
+ const offSync = ((headB - headA) % L + L) % L;
60
+ const offALeadsB = ((headB - headA + 1) % L + L) % L;
61
+ const offBLeadsA = ((headB - headA - 1) % L + L) % L;
62
+ let kSync = 0;
63
+ let kALeadsB = 0;
64
+ let kBLeadsA = 0;
65
+ let densityA = 0;
66
+ let densityB = 0;
6
67
  for (let i = 0; i < L; i++) {
7
- const valA = bufferA[i];
8
- let iB = i + baseOffset;
9
- if (iB >= L) {
10
- iB -= L;
11
- }
12
- const valB = bufferB[iB];
13
- dot += valA * valB;
14
- magA += valA * valA;
15
- magB += valB * valB;
68
+ const a = bufferA[i] ? 1 : 0;
69
+ const b = bufferB[i] ? 1 : 0;
70
+ densityA += a;
71
+ densityB += b;
72
+ let iSync = i + offSync;
73
+ if (iSync >= L) iSync -= L;
74
+ let iALeadsB = i + offALeadsB;
75
+ if (iALeadsB >= L) iALeadsB -= L;
76
+ let iBLeadsA = i + offBLeadsA;
77
+ if (iBLeadsA >= L) iBLeadsA -= L;
78
+ if (a && bufferB[iSync]) kSync++;
79
+ if (a && bufferB[iALeadsB]) kALeadsB++;
80
+ if (a && bufferB[iBLeadsA]) kBLeadsA++;
16
81
  }
17
- if (magA === 0 || magB === 0) return 0;
18
- return dot / (Math.sqrt(magA) * Math.sqrt(magB));
82
+ return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
19
83
  };
20
- var calculateCosineSimilarity = (A, B) => {
21
- let dot = 0, magA = 0, magB = 0;
22
- for (let i = 0; i < A.length; i++) {
23
- dot += A[i] * B[i];
24
- magA += A[i] * A[i];
25
- magB += B[i] * B[i];
26
- }
27
- return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
84
+ var cosineFromOverlap = (overlap, densityA, densityB) => {
85
+ if (densityA <= 0 || densityB <= 0) return 0;
86
+ return overlap / Math.sqrt(densityA * densityB);
28
87
  };
29
88
 
30
89
  // src/core/graph.ts
@@ -61,13 +120,27 @@ var calculateSpectralInfluence = (graph, maxIterations = 20, tolerance = 1e-3) =
61
120
  }
62
121
  return scores;
63
122
  };
64
-
65
- // src/core/constants.ts
66
- var WINDOW_SIZE = 50;
67
- var SIMILARITY_THRESHOLD = 0.88;
68
- var LOOP_THRESHOLD = 150;
69
- var VOLATILITY_THRESHOLD = 25;
70
- var INSTANCE_SEP = "##";
123
+ var groupEventSources = (nodes, edges) => {
124
+ const outgoing = /* @__PURE__ */ new Map();
125
+ edges.forEach((e) => {
126
+ if (!outgoing.has(e.source)) outgoing.set(e.source, []);
127
+ outgoing.get(e.source).push(e);
128
+ });
129
+ const eventSourceIds = nodes.filter((n) => n.role === "event").map((n) => n.id);
130
+ const signatureOf = (sourceEdges) => sourceEdges.map((e) => `${e.target}@${e.weight}`).sort().join("|");
131
+ const buckets = /* @__PURE__ */ new Map();
132
+ eventSourceIds.forEach((id) => {
133
+ const sig = signatureOf(outgoing.get(id) || []);
134
+ if (!buckets.has(sig)) buckets.set(sig, []);
135
+ buckets.get(sig).push(id);
136
+ });
137
+ const groups = Array.from(buckets.values()).map((sourceIds) => ({
138
+ sourceIds,
139
+ occurrences: sourceIds.length,
140
+ edges: (outgoing.get(sourceIds[0]) || []).slice()
141
+ }));
142
+ return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
143
+ };
71
144
 
72
145
  // src/core/label.ts
73
146
  var stripInstance = (label) => {
@@ -80,6 +153,7 @@ var parseLabel = (label) => {
80
153
  return { file: parts[0] || "Unknown", name: parts[1] || base };
81
154
  };
82
155
  var isSameField = (labelA, labelB) => stripInstance(labelA) === stripInstance(labelB);
156
+ var isEffectLabel = (name) => /^effect_L\d+(:\d+)?$/.test(name) || name === "anonymous_effect" || name === "anonymous_layout_effect";
83
157
 
84
158
  // src/core/ranker.ts
85
159
  var identifyTopIssues = (graph, history2, redundantLabels2, violationMap) => {
@@ -181,32 +255,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
181
255
  var LOG_COOLDOWN = 3e3;
182
256
  var THEME = {
183
257
  identity: "#6C5CE7",
184
- // Purple (Brand)
185
258
  problem: "#D63031",
186
- // Red (Bugs)
187
259
  solution: "#FBC531",
188
- // Yellow (Fixes)
189
260
  context: "#0984E3",
190
- // Blue (Locations)
191
261
  muted: "#9AA0A6",
192
- // Gray (Metadata)
193
262
  border: "#2E2E35",
194
263
  success: "#00b894"
195
- // Green (Good Score)
196
264
  };
197
265
  var STYLES = {
198
- // Structure
199
266
  basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
200
267
  headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
201
268
  headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
202
269
  version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
203
- // Actions
204
270
  actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
205
271
  actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
206
- // Context
207
272
  impactLabel: `color: ${THEME.context}; font-weight: bold;`,
208
273
  location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
209
- // Text
210
274
  subText: `color: ${THEME.muted}; font-size: 11px;`,
211
275
  bold: "font-weight: bold;",
212
276
  label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
@@ -221,6 +285,15 @@ var shouldLog = (key) => {
221
285
  return false;
222
286
  };
223
287
  var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
288
+ var areSyncSignificant = (metaA, metaB) => {
289
+ const { kSync, densityA, densityB } = countOverlapsCircular(
290
+ metaA.buffer,
291
+ metaA.head,
292
+ metaB.buffer,
293
+ metaB.head
294
+ );
295
+ return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
296
+ };
224
297
  var getSuggestedFix = (issue, info) => {
225
298
  if (issue.label.includes("Global Event")) {
226
299
  return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
@@ -250,7 +323,7 @@ var getSuggestedFix = (issue, info) => {
250
323
  }
251
324
  return `Check the dependency chain of ${info.name}.`;
252
325
  };
253
- var displayHealthReport = (history2, threshold, violationMap) => {
326
+ var displayHealthReport = (history2, violationMap) => {
254
327
  if (!isWeb) return;
255
328
  const entries = Array.from(history2.entries());
256
329
  if (entries.length === 0) return;
@@ -326,11 +399,10 @@ var displayHealthReport = (history2, threshold, violationMap) => {
326
399
  processed.add(labelA);
327
400
  entries.forEach(([labelB, metaB]) => {
328
401
  if (labelA === labelB || processed.has(labelB)) return;
329
- if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
330
- if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
331
- currentCluster.push(labelB);
332
- processed.add(labelB);
333
- }
402
+ if (!areSyncSignificant(metaA, metaB)) return;
403
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
404
+ currentCluster.push(labelB);
405
+ processed.add(labelB);
334
406
  });
335
407
  if (currentCluster.length > 1) clusters.push(currentCluster);
336
408
  else independentCount++;
@@ -406,7 +478,7 @@ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
406
478
  const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
407
479
  console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
408
480
  console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
409
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, STYLES.bold, "");
481
+ console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} overlapped on ${(sim * 100).toFixed(0)}% of aligned updates.`, STYLES.bold, "");
410
482
  if (isContextMirror || isStoreMirror) {
411
483
  const sourceType = isStoreMirror ? "External Store" : "Global Context";
412
484
  console.log(
@@ -475,6 +547,99 @@ var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
475
547
  }
476
548
  console.groupEnd();
477
549
  };
550
+ var splitHookLine = (raw) => {
551
+ const m = raw.match(/^(.*):(\d+)$/);
552
+ if (!m) return { hook: raw };
553
+ return { hook: m[1], line: Number(m[2]) };
554
+ };
555
+ var formatHook = (raw) => {
556
+ const { hook } = splitHookLine(raw);
557
+ if (!isEffectLabel(hook)) return hook;
558
+ const lineMatch = hook.match(/L(\d+)$/);
559
+ return lineMatch ? `effect @ L${lineMatch[1]}` : "effect (anonymous)";
560
+ };
561
+ var formatNode = (node, fallbackId = "?") => {
562
+ if (!node) return fallbackId;
563
+ if (node.role === "event") return "Event";
564
+ const hook = formatHook(node.name || node.id);
565
+ if (node.file && hook) return `${node.file} \u2192 ${hook}`;
566
+ return hook || node.id;
567
+ };
568
+ var displayGraphReport = (graph) => {
569
+ if (!isWeb) return;
570
+ if (graph.nodes.length === 0) {
571
+ console.log(
572
+ `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
573
+ STYLES.headerIdentity,
574
+ `color: ${THEME.muted}; font-style: italic;`
575
+ );
576
+ return;
577
+ }
578
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
579
+ const outgoing = /* @__PURE__ */ new Map();
580
+ graph.edges.forEach((e) => {
581
+ if (!outgoing.has(e.source)) outgoing.set(e.source, []);
582
+ outgoing.get(e.source).push(e);
583
+ });
584
+ const eventGroups = graph.eventGroups.map((g) => ({
585
+ sourceIds: g.sourceIds,
586
+ sourceNode: nodeById.get(g.sourceIds[0]),
587
+ edges: g.edges,
588
+ occurrences: g.occurrences
589
+ }));
590
+ const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
591
+ 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 }));
592
+ const groups = [...eventGroups, ...nonEventGroups].sort(
593
+ (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
594
+ );
595
+ console.group(
596
+ `%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}`,
597
+ STYLES.headerIdentity,
598
+ `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
599
+ );
600
+ console.log(
601
+ `%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
602
+ STYLES.subText
603
+ );
604
+ groups.forEach((group) => {
605
+ const isEvent = group.sourceNode?.role === "event";
606
+ const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
607
+ const isFx = group.sourceNode?.role === "effect";
608
+ const isUnknown = group.sourceNode?.role === "unknown";
609
+ const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
610
+ const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
611
+ const fanout = group.edges.length;
612
+ const hits = group.occurrences;
613
+ const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
614
+ const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
615
+ console.groupCollapsed(
616
+ `%c${icon} %c${title}`,
617
+ `color: ${color};`,
618
+ "font-family: monospace; font-weight: 600;"
619
+ );
620
+ group.edges.slice().sort((a, b) => b.weight - a.weight).forEach((edge) => {
621
+ const target = nodeById.get(edge.target);
622
+ const label = formatNode(target, edge.target);
623
+ const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
624
+ if (target?.redundant) {
625
+ console.log(
626
+ `%c ${label}%c${weight} %credundant`,
627
+ `color: ${THEME.muted}; font-family: monospace;`,
628
+ `color: ${THEME.muted}; font-style: italic;`,
629
+ `color: ${THEME.problem}; font-weight: bold;`
630
+ );
631
+ } else {
632
+ console.log(
633
+ `%c ${label}%c${weight}`,
634
+ `color: ${THEME.muted}; font-family: monospace;`,
635
+ `color: ${THEME.muted}; font-style: italic;`
636
+ );
637
+ }
638
+ });
639
+ console.groupEnd();
640
+ });
641
+ console.groupEnd();
642
+ };
478
643
  var displayViolentBreaker = (label, count, threshold) => {
479
644
  if (!isWeb) return;
480
645
  const { name } = parseLabel(label);
@@ -491,7 +656,6 @@ var displayBootLog = (windowSize) => {
491
656
  };
492
657
 
493
658
  // src/core/analysis.ts
494
- var CAUSAL_MARGIN = 0.05;
495
659
  var isEventDriven = (label, graph) => {
496
660
  for (const [parent, targets] of graph.entries()) {
497
661
  if (parent.startsWith("Event_Tick_") && targets.has(label)) {
@@ -501,28 +665,33 @@ var isEventDriven = (label, graph) => {
501
665
  return false;
502
666
  };
503
667
  var calculateAllSimilarities = (entryA, entryB) => {
504
- const sync = calculateSimilarityCircular(
505
- entryA.meta.buffer,
506
- entryA.meta.head,
507
- entryB.meta.buffer,
508
- entryB.meta.head,
509
- 0
510
- );
511
- const bA = calculateSimilarityCircular(
668
+ const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
512
669
  entryA.meta.buffer,
513
670
  entryA.meta.head,
514
671
  entryB.meta.buffer,
515
- entryB.meta.head,
516
- 1
672
+ entryB.meta.head
517
673
  );
518
- const aB = calculateSimilarityCircular(
519
- entryA.meta.buffer,
520
- entryA.meta.head,
521
- entryB.meta.buffer,
522
- entryB.meta.head,
523
- -1
524
- );
525
- return { sync, bA, aB, max: Math.max(sync, bA, aB) };
674
+ const sync = cosineFromOverlap(kSync, densityA, densityB);
675
+ const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
676
+ const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
677
+ const max = Math.max(sync, bA, aB);
678
+ const windowSize = entryA.meta.buffer.length;
679
+ const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
680
+ const kLead = Math.max(kALeadsB, kBLeadsA);
681
+ const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
682
+ return {
683
+ sync,
684
+ bA,
685
+ aB,
686
+ max,
687
+ kSync,
688
+ kALeadsB,
689
+ kBLeadsA,
690
+ densityA,
691
+ densityB,
692
+ significantSync,
693
+ significantLead
694
+ };
526
695
  };
527
696
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
528
697
  if (entryA.label === entryB.label) return true;
@@ -547,52 +716,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
547
716
  const roleA = entryA.meta.role;
548
717
  const roleB = entryB.meta.role;
549
718
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
550
- if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
719
+ if (similarities.densityA < 2 || similarities.densityB < 2) return;
720
+ const score = similarities.sync;
551
721
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
552
722
  redundantSet.add(entryA.label);
553
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
554
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
723
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: score });
724
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
555
725
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
556
726
  redundantSet.add(entryB.label);
557
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
558
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
727
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: score });
728
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, score);
559
729
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
560
730
  redundantSet.add(entryA.label);
561
731
  redundantSet.add(entryB.label);
562
- pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
563
- pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
564
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
732
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: score });
733
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: score });
734
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
565
735
  }
566
736
  };
567
737
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
568
738
  if (entryA.isVolatile || entryB.isVolatile) return;
569
- if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
570
739
  const addLeak = (source, target) => {
571
740
  if (isEventDriven(target, graph)) return;
572
- if (!violationMap.has(source)) {
573
- violationMap.set(source, []);
574
- }
575
- violationMap.get(source).push({ type: "causal_leak", target });
741
+ pushViolation(violationMap, source, { type: "causal_leak", target });
576
742
  const sourceEntry = source === entryA.label ? entryA : entryB;
577
743
  const targetEntry = source === entryA.label ? entryB : entryA;
578
744
  displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
579
745
  };
580
- if (similarities.bA === similarities.max) {
746
+ if (similarities.kALeadsB >= similarities.kBLeadsA) {
581
747
  addLeak(entryA.label, entryB.label);
582
- } else if (similarities.aB === similarities.max) {
748
+ } else {
583
749
  addLeak(entryB.label, entryA.label);
584
750
  }
585
751
  };
586
752
  var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
587
- let compCount = 0;
588
753
  const violationMap = /* @__PURE__ */ new Map();
754
+ let compCount = 0;
589
755
  for (const entryA of dirtyEntries) {
590
756
  for (const entryB of allEntries) {
591
757
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
592
758
  compCount++;
593
759
  const similarities = calculateAllSimilarities(entryA, entryB);
594
- if (similarities.max > SIMILARITY_THRESHOLD) {
760
+ if (similarities.significantSync) {
595
761
  detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
762
+ }
763
+ if (similarities.significantLead) {
596
764
  detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
597
765
  }
598
766
  }
@@ -851,9 +1019,9 @@ var beginEffectTracking = (l) => {
851
1019
  var endEffectTracking = () => {
852
1020
  instance.currentEffectSource = null;
853
1021
  };
854
- var printBasisHealthReport = (threshold = 0.5) => {
1022
+ var printBasisHealthReport = () => {
855
1023
  if (!instance.config.debug) return;
856
- displayHealthReport(instance.history, threshold, instance.violationMap);
1024
+ displayHealthReport(instance.history, instance.violationMap);
857
1025
  };
858
1026
  var getBasisMetrics = () => ({
859
1027
  engine: "v0.6.x",
@@ -861,9 +1029,53 @@ var getBasisMetrics = () => ({
861
1029
  analysis_ms: instance.metrics.lastAnalysisTimeMs.toFixed(3),
862
1030
  entropy: instance.metrics.systemEntropy.toFixed(3)
863
1031
  });
1032
+ var getBasisGraph = () => {
1033
+ const nodeIds = /* @__PURE__ */ new Set();
1034
+ const edges = [];
1035
+ instance.graph.forEach((targets, source) => {
1036
+ nodeIds.add(source);
1037
+ targets.forEach((weight, target) => {
1038
+ nodeIds.add(target);
1039
+ edges.push({ source, target, weight });
1040
+ });
1041
+ });
1042
+ const nodes = Array.from(nodeIds).map((id) => {
1043
+ if (id.startsWith("Event_Tick_")) {
1044
+ return { id, name: "Event", file: "(shared trigger)", role: "event", density: null, redundant: false };
1045
+ }
1046
+ const meta = instance.history.get(id);
1047
+ const { file, name } = parseLabel(id);
1048
+ if (!meta) {
1049
+ const role = isEffectLabel(name) ? "effect" : "unknown";
1050
+ return { id, name, file, role, density: null, redundant: false };
1051
+ }
1052
+ return {
1053
+ id,
1054
+ name,
1055
+ file,
1056
+ role: meta.role,
1057
+ density: meta.density,
1058
+ redundant: instance.redundantLabels.has(id)
1059
+ };
1060
+ });
1061
+ return {
1062
+ generatedAt: Date.now(),
1063
+ bufferWindowSize: WINDOW_SIZE,
1064
+ eventTtlMs: EVENT_TTL,
1065
+ nodes,
1066
+ edges,
1067
+ eventGroups: groupEventSources(nodes, edges)
1068
+ };
1069
+ };
1070
+ var printBasisGraph = () => {
1071
+ if (!instance.config.debug) return;
1072
+ displayGraphReport(getBasisGraph());
1073
+ };
864
1074
  if (typeof window !== "undefined") {
865
1075
  window.printBasisReport = printBasisHealthReport;
866
1076
  window.getBasisMetrics = getBasisMetrics;
1077
+ window.getBasisGraph = getBasisGraph;
1078
+ window.printBasisGraph = printBasisGraph;
867
1079
  }
868
1080
 
869
1081
  export {
@@ -879,6 +1091,8 @@ export {
879
1091
  beginEffectTracking,
880
1092
  endEffectTracking,
881
1093
  printBasisHealthReport,
882
- getBasisMetrics
1094
+ getBasisMetrics,
1095
+ getBasisGraph,
1096
+ printBasisGraph
883
1097
  };
884
- //# sourceMappingURL=chunk-6GBZSOB3.mjs.map
1098
+ //# sourceMappingURL=chunk-WMHY2C6D.mjs.map