react-state-basis 0.6.5 → 0.6.7

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
@@ -64,33 +64,92 @@ module.exports = __toCommonJS(index_exports);
64
64
  var React = __toESM(require("react"));
65
65
  var import_react = require("react");
66
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
+
67
75
  // src/core/math.ts
68
- 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) => {
69
124
  const L = bufferA.length;
70
- let dot = 0, magA = 0, magB = 0;
71
- 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;
72
133
  for (let i = 0; i < L; i++) {
73
- const valA = bufferA[i];
74
- let iB = i + baseOffset;
75
- if (iB >= L) {
76
- iB -= L;
77
- }
78
- const valB = bufferB[iB];
79
- dot += valA * valB;
80
- magA += valA * valA;
81
- 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++;
82
147
  }
83
- if (magA === 0 || magB === 0) return 0;
84
- return dot / (Math.sqrt(magA) * Math.sqrt(magB));
148
+ return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
85
149
  };
86
- var calculateCosineSimilarity = (A, B) => {
87
- let dot = 0, magA = 0, magB = 0;
88
- for (let i = 0; i < A.length; i++) {
89
- dot += A[i] * B[i];
90
- magA += A[i] * A[i];
91
- magB += B[i] * B[i];
92
- }
93
- 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);
94
153
  };
95
154
 
96
155
  // src/core/graph.ts
@@ -149,13 +208,6 @@ var groupEventSources = (nodes, edges) => {
149
208
  return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
150
209
  };
151
210
 
152
- // src/core/constants.ts
153
- var WINDOW_SIZE = 50;
154
- var SIMILARITY_THRESHOLD = 0.88;
155
- var LOOP_THRESHOLD = 150;
156
- var VOLATILITY_THRESHOLD = 25;
157
- var INSTANCE_SEP = "##";
158
-
159
211
  // src/core/label.ts
160
212
  var stripInstance = (label) => {
161
213
  const idx = label.indexOf(INSTANCE_SEP);
@@ -269,32 +321,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
269
321
  var LOG_COOLDOWN = 3e3;
270
322
  var THEME = {
271
323
  identity: "#6C5CE7",
272
- // Purple (Brand)
273
324
  problem: "#D63031",
274
- // Red (Bugs)
275
325
  solution: "#FBC531",
276
- // Yellow (Fixes)
277
326
  context: "#0984E3",
278
- // Blue (Locations)
279
327
  muted: "#9AA0A6",
280
- // Gray (Metadata)
281
328
  border: "#2E2E35",
282
329
  success: "#00b894"
283
- // Green (Good Score)
284
330
  };
285
331
  var STYLES = {
286
- // Structure
287
332
  basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
288
333
  headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
289
334
  headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
290
335
  version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
291
- // Actions
292
336
  actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
293
337
  actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
294
- // Context
295
338
  impactLabel: `color: ${THEME.context}; font-weight: bold;`,
296
339
  location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
297
- // Text
298
340
  subText: `color: ${THEME.muted}; font-size: 11px;`,
299
341
  bold: "font-weight: bold;",
300
342
  label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
@@ -309,97 +351,95 @@ var shouldLog = (key) => {
309
351
  return false;
310
352
  };
311
353
  var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
354
+ var displayName = (raw) => {
355
+ const { name } = parseLabel(raw);
356
+ return name.replace(/:\d+$/, "");
357
+ };
358
+ var areSyncSignificant = (metaA, metaB) => {
359
+ const { kSync, densityA, densityB } = countOverlapsCircular(
360
+ metaA.buffer,
361
+ metaA.head,
362
+ metaB.buffer,
363
+ metaB.head
364
+ );
365
+ return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
366
+ };
312
367
  var getSuggestedFix = (issue, info) => {
313
368
  if (issue.label.includes("Global Event")) {
314
- return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
369
+ return `One interaction is updating state in several places. If that is really one transition, put it in one %cstore / reducer%c. If it is intentional, ignore this.`;
315
370
  }
316
371
  const violations = issue.violations || [];
317
372
  const leaks = violations.filter((v) => v.type === "causal_leak");
318
373
  const mirrors = violations.filter((v) => v.type === "context_mirror");
319
374
  const duplicates = violations.filter((v) => v.type === "duplicate_state");
320
375
  if (mirrors.length > 0) {
321
- return `Local state is 'shadowing' Global Context. This creates two sources of truth. Delete the local state and consume the %cContext%c value directly.`;
376
+ return `A local hook is only tracking context or a store. If it is not a draft, delete it and read the %ccontext / store%c in render.`;
322
377
  }
323
378
  if (leaks.length > 0) {
324
- const targetName = parseLabel(leaks[0].target).name;
379
+ const targetName = displayName(leaks[0].target);
325
380
  if (issue.label.includes("effect")) {
326
- return `This Effect triggers a synchronous re-render of ${targetName}. Calculate ${targetName} during the render phase (Derived State) or wrap in %cuseMemo%c if expensive.`;
381
+ return `An effect is calling setState on ${targetName}, which paints again. If you can compute ${targetName} while rendering, drop the %ceffect%c.`;
327
382
  }
328
- return `State cascading detected. ${info.name} triggers ${targetName} in a separate frame. Merge them into one object to update simultaneously.`;
383
+ return `${info.name} updates, then ${targetName} updates on the next frame. If they are one fact, write them in the same %csetState%c.`;
329
384
  }
330
385
  if (duplicates.length > 0) {
331
386
  if (isBooleanLike(info.name)) {
332
- return `Boolean Explosion detected. Multiple flags are toggling in sync. Replace impossible states with a single %cstatus%c string ('idle' | 'loading' | 'success').`;
387
+ return `Several flags move together. One %cstatus%c value avoids impossible combinations.`;
333
388
  }
334
- return `Redundant State detected. This variable carries no unique information. Derive it from the source variable during render, or use %cuseMemo%c to cache the result.`;
389
+ return `These hooks move together. If one is just the other in another shape, compute it while %crendering%c.`;
335
390
  }
336
391
  if (issue.metric === "density") {
337
- return `High-Frequency Update. This variable updates faster than the frame rate. Apply %cdebounce%c or move to a Ref to unblock the main thread.`;
392
+ return `This hook updates faster than a frame. %cDebounce%c it or keep it in a ref if the UI does not need every pulse.`;
338
393
  }
339
- return `Check the dependency chain of ${info.name}.`;
394
+ return `Inspect ${info.name} and what updates with it.`;
340
395
  };
341
- var displayHealthReport = (history2, threshold, violationMap) => {
396
+ var displayHealthReport = (history2, violationMap) => {
342
397
  if (!isWeb) return;
343
398
  const entries = Array.from(history2.entries());
344
399
  if (entries.length === 0) return;
345
400
  const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
346
- console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerIdentity);
401
+ console.group(`%c BASIS | report `, STYLES.headerIdentity);
347
402
  if (topIssues.length > 0) {
348
- console.log(
349
- `%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
350
- `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
351
- `font-weight: normal; color: ${THEME.muted}; font-style: italic;`
352
- );
403
+ console.log(`%cStart here`, `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`);
353
404
  topIssues.forEach((issue, idx) => {
354
405
  const info = parseLabel(issue.label);
355
- const icon = issue.metric === "influence" ? "\u26A1" : "\u{1F4C8}";
406
+ const icon = issue.metric === "influence" ? "\u2192" : "\u2022";
356
407
  const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
357
- let displayName = info.name;
358
- let displayFile = info.file;
359
- if (issue.label.includes("Global Event")) {
360
- displayName = info.name;
361
- displayFile = info.file;
362
- }
363
408
  console.group(
364
- ` %c${idx + 1}%c ${icon} ${displayName} %c(${displayFile})`,
409
+ ` %c${idx + 1}%c ${icon} ${displayName(issue.label)} %c(${info.file})`,
365
410
  `background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
366
411
  "font-family: monospace; font-weight: 700;",
367
- `color: ${THEME.muted}; font-size: 10px; font-weight: normal; font-style: italic;`
412
+ `color: ${THEME.muted}; font-size: 10px; font-weight: normal;`
368
413
  );
369
- console.log(`%c${issue.reason}`, `color: ${THEME.muted}; font-style: italic;`);
414
+ console.log(`%c${issue.reason}`, `color: ${THEME.muted};`);
370
415
  if (issue.violations.length > 0) {
371
416
  const byFile = /* @__PURE__ */ new Map();
372
417
  issue.violations.forEach((v) => {
373
418
  if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
374
419
  const { file, name } = parseLabel(v.target);
375
420
  if (!byFile.has(file)) byFile.set(file, []);
376
- byFile.get(file).push(name);
421
+ byFile.get(file).push(name.replace(/:\d+$/, ""));
377
422
  });
378
423
  const impactParts = [];
379
424
  byFile.forEach((vars, file) => {
380
- const varList = vars.join(", ");
381
- impactParts.push(`${file} (${varList})`);
425
+ impactParts.push(`${file} (${vars.join(", ")})`);
382
426
  });
383
427
  if (impactParts.length > 0) {
384
- console.log(`%cImpacts: %c${impactParts.join(" + ")}`, STYLES.impactLabel, "");
428
+ console.log(`%cAlso updates: %c${impactParts.join(" \xB7 ")}`, STYLES.impactLabel, "");
385
429
  }
386
430
  }
387
431
  const fix = getSuggestedFix(issue, info);
388
432
  const fixParts = fix.split("%c");
389
433
  if (fixParts.length === 3) {
390
434
  console.log(
391
- `%cSolution: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
435
+ `%cTry: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
392
436
  STYLES.actionLabel,
393
437
  "",
394
438
  STYLES.actionPill,
395
439
  ""
396
440
  );
397
441
  } else {
398
- console.log(
399
- `%cSolution: %c${fix}`,
400
- STYLES.actionLabel,
401
- ""
402
- );
442
+ console.log(`%cTry: %c${fix}`, STYLES.actionLabel, "");
403
443
  }
404
444
  console.groupEnd();
405
445
  });
@@ -414,145 +454,179 @@ var displayHealthReport = (history2, threshold, violationMap) => {
414
454
  processed.add(labelA);
415
455
  entries.forEach(([labelB, metaB]) => {
416
456
  if (labelA === labelB || processed.has(labelB)) return;
417
- if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
418
- if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
419
- currentCluster.push(labelB);
420
- processed.add(labelB);
421
- }
457
+ if (!areSyncSignificant(metaA, metaB)) return;
458
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
459
+ currentCluster.push(labelB);
460
+ processed.add(labelB);
422
461
  });
423
462
  if (currentCluster.length > 1) clusters.push(currentCluster);
424
463
  else independentCount++;
425
464
  });
426
465
  const totalVars = entries.length;
427
- const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
428
- let internalEdges = 0;
429
- instance.graph.forEach((targets, source) => {
430
- if (source.startsWith("Event_Tick_")) return;
431
- internalEdges += targets.size;
432
- });
433
- const causalPenalty = internalEdges / totalVars * 100;
434
- let healthScore = redundancyScore - causalPenalty;
435
- if (healthScore < 0) healthScore = 0;
436
- const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
437
466
  console.log(
438
- `%cSystem Efficiency: %c${healthScore.toFixed(1)}%`,
439
- STYLES.bold,
440
- `color: ${scoreColor}; font-weight: bold;`
467
+ `%c${independentCount + clusters.length} of ${totalVars} instrumented hooks look independent in this window.`,
468
+ STYLES.subText
441
469
  );
442
- console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
443
470
  if (clusters.length > 0) {
444
- console.log(`%cDetected ${clusters.length} Sync Issues:`, `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`);
471
+ console.log(
472
+ `%c${clusters.length} group${clusters.length === 1 ? "" : "s"} that keep updating together:`,
473
+ `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`
474
+ );
445
475
  clusters.forEach((cluster, idx) => {
446
476
  const clusterMetas = cluster.map((l) => ({
447
477
  label: l,
448
478
  meta: history2.get(l),
449
- name: parseLabel(l).name
479
+ name: displayName(l)
450
480
  }));
451
481
  const hasCtx = clusterMetas.some(
452
482
  (c) => c.meta.role === "context" /* CONTEXT */ || c.meta.role === "store" /* STORE */
453
483
  );
454
- const names = clusterMetas.map((c) => {
455
- const prefix = c.meta.role === "store" /* STORE */ ? "\u03A3 " : c.meta.role === "context" /* CONTEXT */ ? "\u03A9 " : "";
456
- return `${prefix}${c.name}`;
457
- }).join(" \u27F7 ");
458
- console.group(` %c${idx + 1}%c ${names}`, `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`, "font-family: monospace; font-weight: bold;");
484
+ const names = clusterMetas.map((c) => c.name).join(", ");
485
+ console.group(
486
+ ` %c${idx + 1}%c ${names}`,
487
+ `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`,
488
+ "font-family: monospace; font-weight: bold;"
489
+ );
459
490
  if (hasCtx) {
460
491
  const hasStore = clusterMetas.some((c) => c.meta.role === "store" /* STORE */);
461
- const sourceType = hasStore ? "External Store" : "global context";
462
- console.log(`%cDiagnosis: ${hasStore ? "Store" : "Context"} Mirroring. Local state is shadowing ${sourceType}.`, `color: ${THEME.problem};`);
463
- console.log(`%cSolution: Use ${sourceType} directly to avoid state drift.`, STYLES.actionLabel);
492
+ const sourceType = hasStore ? "a store" : "context";
493
+ console.log(`A local hook is only following ${sourceType}.`);
494
+ console.log(
495
+ `%cTry:%c Read ${sourceType} in render if the local value is not a draft.`,
496
+ STYLES.actionLabel,
497
+ ""
498
+ );
464
499
  } else {
465
- const boolKeywords = ["is", "has", "can", "should", "loading", "success", "error", "active", "enabled", "open", "visible"];
500
+ const boolKeywords = [
501
+ "is",
502
+ "has",
503
+ "can",
504
+ "should",
505
+ "loading",
506
+ "success",
507
+ "error",
508
+ "active",
509
+ "enabled",
510
+ "open",
511
+ "visible"
512
+ ];
466
513
  const boolCount = clusterMetas.filter(
467
514
  (c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
468
515
  ).length;
469
- const isBoolExplosion = cluster.length > 2 && boolCount / cluster.length > 0.5;
470
- if (isBoolExplosion) {
471
- console.log(`%cDiagnosis:%c Boolean Explosion. Multiple booleans updating in sync.`, STYLES.bold, "");
472
- console.log(`%cSolution:%c Combine into a single %cstatus%c string or a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "", STYLES.actionPill, "");
516
+ if (cluster.length > 2 && boolCount / cluster.length > 0.5) {
517
+ console.log(`These flags move together.`);
518
+ console.log(
519
+ `%cTry:%c One %cstatus%c instead of several booleans.`,
520
+ STYLES.actionLabel,
521
+ "",
522
+ STYLES.actionPill,
523
+ ""
524
+ );
473
525
  } else if (cluster.length > 2) {
474
- console.log(`%cDiagnosis:%c Sibling Updates. These states respond to the same event.`, STYLES.bold, "");
475
- console.log(`%cSolution:%c This may be intentional. If not, consolidate into a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
526
+ console.log(`These hooks move on the same frames. Often the same click or fetch.`);
527
+ console.log(
528
+ `%cTry:%c Leave it if that is intentional. Otherwise one %creducer%c.`,
529
+ STYLES.actionLabel,
530
+ "",
531
+ STYLES.actionPill,
532
+ ""
533
+ );
476
534
  } else {
477
- console.log(`%cDiagnosis:%c Redundant State. Variables always change together.`, STYLES.bold, "");
478
- console.log(`%cSolution:%c Derive one from the other via %cuseMemo%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
535
+ console.log(`These two hooks keep updating in the same frame.`);
536
+ console.log(
537
+ `%cTry:%c If one is derived, compute it while %crendering%c.`,
538
+ STYLES.actionLabel,
539
+ "",
540
+ STYLES.actionPill,
541
+ ""
542
+ );
479
543
  }
480
544
  }
481
545
  console.groupEnd();
482
546
  });
483
547
  } else {
484
- console.log("%c\u2728 Your architecture is clean. No redundant state detected.", `color: ${THEME.success}; font-weight: bold;`);
548
+ console.log(
549
+ "%cNo hooks were updating in lockstep in this window.",
550
+ `color: ${THEME.success}; font-weight: bold;`
551
+ );
485
552
  }
486
553
  console.groupEnd();
487
554
  };
488
- var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
555
+ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, overlap) => {
489
556
  if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
490
557
  const infoA = parseLabel(labelA);
491
- const infoB = parseLabel(labelB);
558
+ const nameA = displayName(labelA);
559
+ const nameB = displayName(labelB);
492
560
  const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
493
561
  const isStoreMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "store" /* STORE */ || metaB.role === "local" /* LOCAL */ && metaA.role === "store" /* STORE */;
494
- const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
495
- console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
496
- console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
497
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, STYLES.bold, "");
562
+ const alertType = isContextMirror ? "local state follows context" : isStoreMirror ? "local state follows a store" : "hooks moving together";
563
+ const times = overlap.kSync === 1 ? "time" : "times";
564
+ console.group(`%c BASIS | ${alertType} `, STYLES.headerProblem);
565
+ console.log(`%c${infoA.file}`, STYLES.location);
566
+ console.log(
567
+ `%c${nameA}%c and %c${nameB}%c updated in the same frame ${overlap.kSync} ${times}.`,
568
+ STYLES.label,
569
+ "",
570
+ STYLES.label,
571
+ ""
572
+ );
498
573
  if (isContextMirror || isStoreMirror) {
499
- const sourceType = isStoreMirror ? "External Store" : "Global Context";
574
+ const sourceType = isStoreMirror ? "store" : "context";
575
+ console.log(
576
+ `%cTry:%c If this is not a draft, delete the local hook and read the %c${sourceType}%c in render.`,
577
+ STYLES.bold,
578
+ "",
579
+ STYLES.actionPill,
580
+ ""
581
+ );
582
+ } else if (isBooleanLike(nameA) || isBooleanLike(nameB)) {
500
583
  console.log(
501
- `%cFix:%c Local state is 'shadowing' ${sourceType}. Delete the local state and consume the %c${sourceType}%c value directly.`,
584
+ `%cTry:%c One %cstatus%c instead of several flags.`,
502
585
  STYLES.bold,
503
586
  "",
504
587
  STYLES.actionPill,
505
588
  ""
506
589
  );
507
590
  } else {
508
- if (isBooleanLike(infoA.name) || isBooleanLike(infoB.name)) {
509
- console.log(
510
- `%cFix:%c Boolean Explosion detected. Merge flags into a single %cstatus%c string or %cuseReducer%c.`,
511
- STYLES.bold,
512
- "",
513
- STYLES.actionPill,
514
- "",
515
- STYLES.actionPill,
516
- ""
517
- );
518
- } else {
519
- console.log(
520
- `%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
521
- STYLES.bold,
522
- "",
523
- STYLES.label,
524
- "",
525
- STYLES.label,
526
- "",
527
- STYLES.actionPill,
528
- ""
529
- );
530
- }
591
+ console.log(
592
+ `%cTry:%c If %c${nameB}%c is just %c${nameA}%c in another shape, compute it while rendering.`,
593
+ STYLES.bold,
594
+ "",
595
+ STYLES.label,
596
+ "",
597
+ STYLES.label,
598
+ ""
599
+ );
531
600
  }
532
601
  console.groupEnd();
533
602
  };
534
- var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
603
+ var displayCausalHint = (targetLabel, _targetMeta, sourceLabel, sourceMeta) => {
535
604
  if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
536
605
  const target = parseLabel(targetLabel);
537
- const source = parseLabel(sourceLabel);
538
- const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "CONTEXT SYNC LEAK" : sourceMeta.role === "store" /* STORE */ ? "STORE SYNC LEAK" : "DOUBLE RENDER";
606
+ const sourceName = displayName(sourceLabel);
607
+ const targetName = displayName(targetLabel);
608
+ const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "extra render from context" : sourceMeta.role === "store" /* STORE */ ? "extra render from a store" : "extra render";
539
609
  const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
540
- console.groupCollapsed(`%c \u26A1 BASIS | ${headerType} `, STYLES.headerProblem);
541
- console.log(`%c\u{1F4CD} Location: %c${target.file}`, STYLES.bold, STYLES.location);
542
- console.log(`%cIssue:%c ${source.name} triggers ${target.name} in separate frames.`, STYLES.bold, "");
610
+ console.groupCollapsed(`%c BASIS | ${headerType} `, STYLES.headerProblem);
611
+ console.log(`%c${target.file}`, STYLES.location);
612
+ console.log(
613
+ `%c${sourceName}%c updates %c${targetName}%c on the next frame.`,
614
+ STYLES.label,
615
+ "",
616
+ STYLES.label,
617
+ ""
618
+ );
543
619
  if (isEffect) {
544
620
  console.log(
545
- `%cFix:%c Derive %c${target.name}%c during the render phase (remove effect) or wrap in %cuseMemo%c.`,
621
+ `%cTry:%c If %c${targetName}%c can be computed while rendering, drop the extra setState.`,
546
622
  STYLES.bold,
547
623
  "",
548
624
  STYLES.label,
549
- "",
550
- STYLES.actionPill,
551
625
  ""
552
626
  );
553
627
  } else {
554
628
  console.log(
555
- `%cFix:%c Merge %c${target.name}%c with %c${source.name}%c into a single state update.`,
629
+ `%cTry:%c Write %c${targetName}%c in the same update as %c${sourceName}%c if they are one fact.`,
556
630
  STYLES.bold,
557
631
  "",
558
632
  STYLES.label,
@@ -585,7 +659,7 @@ var displayGraphReport = (graph) => {
585
659
  if (!isWeb) return;
586
660
  if (graph.nodes.length === 0) {
587
661
  console.log(
588
- `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
662
+ `%c BASIS | update graph %c(nothing recorded yet)`,
589
663
  STYLES.headerIdentity,
590
664
  `color: ${THEME.muted}; font-style: italic;`
591
665
  );
@@ -604,17 +678,22 @@ var displayGraphReport = (graph) => {
604
678
  occurrences: g.occurrences
605
679
  }));
606
680
  const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
607
- 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 }));
681
+ const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({
682
+ sourceIds: [id],
683
+ sourceNode: nodeById.get(id),
684
+ edges: outgoing.get(id),
685
+ occurrences: 1
686
+ }));
608
687
  const groups = [...eventGroups, ...nonEventGroups].sort(
609
688
  (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
610
689
  );
611
690
  console.group(
612
- `%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}`,
691
+ `%c BASIS | update graph %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 last ${graph.bufferWindowSize} frames`,
613
692
  STYLES.headerIdentity,
614
- `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
693
+ `color: ${THEME.muted}; font-weight: normal;`
615
694
  );
616
695
  console.log(
617
- `%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
696
+ `%cparent \u2192 child = what we saw cause an update. (\xD7N) = times in this window. Repeat clicks with the same targets are grouped.`,
618
697
  STYLES.subText
619
698
  );
620
699
  groups.forEach((group) => {
@@ -622,12 +701,12 @@ var displayGraphReport = (graph) => {
622
701
  const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
623
702
  const isFx = group.sourceNode?.role === "effect";
624
703
  const isUnknown = group.sourceNode?.role === "unknown";
625
- const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
704
+ const icon = isEvent ? "\u2022" : isCtx ? "ctx" : isFx ? "fx" : isUnknown ? "?" : "\u2022";
626
705
  const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
627
706
  const fanout = group.edges.length;
628
707
  const hits = group.occurrences;
629
708
  const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
630
- const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
709
+ const title = isEvent ? `click / event \xB7 ${fanout} update${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
631
710
  console.groupCollapsed(
632
711
  `%c${icon} %c${title}`,
633
712
  `color: ${color};`,
@@ -639,16 +718,16 @@ var displayGraphReport = (graph) => {
639
718
  const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
640
719
  if (target?.redundant) {
641
720
  console.log(
642
- `%c ${label}%c${weight} %credundant`,
721
+ `%c ${label}%c${weight} %cmoving with another hook`,
643
722
  `color: ${THEME.muted}; font-family: monospace;`,
644
- `color: ${THEME.muted}; font-style: italic;`,
723
+ `color: ${THEME.muted};`,
645
724
  `color: ${THEME.problem}; font-weight: bold;`
646
725
  );
647
726
  } else {
648
727
  console.log(
649
728
  `%c ${label}%c${weight}`,
650
729
  `color: ${THEME.muted}; font-family: monospace;`,
651
- `color: ${THEME.muted}; font-style: italic;`
730
+ `color: ${THEME.muted};`
652
731
  );
653
732
  }
654
733
  });
@@ -656,23 +735,29 @@ var displayGraphReport = (graph) => {
656
735
  });
657
736
  console.groupEnd();
658
737
  };
659
- var displayViolentBreaker = (label, count, threshold) => {
738
+ var displayViolentBreaker = (label, count, _threshold) => {
660
739
  if (!isWeb) return;
661
- const { name } = parseLabel(label);
662
- console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, STYLES.headerProblem);
663
- console.error(`INFINITE LOOP DETECTED
664
- Variable: ${name}
665
- Frequency: ${count} updates/sec`);
666
- console.log(`%cACTION: Update BLOCKED to prevent browser freeze.`, `color: ${THEME.problem}; font-weight: bold;`);
740
+ const name = displayName(label);
741
+ console.group(`%c BASIS | loop guard `, STYLES.headerProblem);
742
+ console.error(
743
+ `${name} updated ${count} times in one second. Basis stopped recording this path so the tab stays usable.`
744
+ );
745
+ console.log(
746
+ `%cReact may still error on its own. Fix the effect that writes a value it also lists as a dependency.`,
747
+ `color: ${THEME.muted};`
748
+ );
667
749
  console.groupEnd();
668
750
  };
669
751
  var displayBootLog = (windowSize) => {
670
752
  if (!isWeb) return;
671
- console.log(`%cBasis%cAuditor%c "Graph Era" (Window: ${windowSize})`, STYLES.basis, STYLES.version, `color: ${THEME.muted}; font-style: italic; margin-left: 8px;`);
753
+ console.log(
754
+ `%cBasis%c watching updates (${windowSize}-frame window)`,
755
+ STYLES.basis,
756
+ `color: ${THEME.muted}; margin-left: 8px;`
757
+ );
672
758
  };
673
759
 
674
760
  // src/core/analysis.ts
675
- var CAUSAL_MARGIN = 0.05;
676
761
  var isEventDriven = (label, graph) => {
677
762
  for (const [parent, targets] of graph.entries()) {
678
763
  if (parent.startsWith("Event_Tick_") && targets.has(label)) {
@@ -682,29 +767,40 @@ var isEventDriven = (label, graph) => {
682
767
  return false;
683
768
  };
684
769
  var calculateAllSimilarities = (entryA, entryB) => {
685
- const sync = calculateSimilarityCircular(
686
- entryA.meta.buffer,
687
- entryA.meta.head,
688
- entryB.meta.buffer,
689
- entryB.meta.head,
690
- 0
691
- );
692
- const bA = calculateSimilarityCircular(
770
+ const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
693
771
  entryA.meta.buffer,
694
772
  entryA.meta.head,
695
773
  entryB.meta.buffer,
696
- entryB.meta.head,
697
- 1
774
+ entryB.meta.head
698
775
  );
699
- const aB = calculateSimilarityCircular(
700
- entryA.meta.buffer,
701
- entryA.meta.head,
702
- entryB.meta.buffer,
703
- entryB.meta.head,
704
- -1
705
- );
706
- return { sync, bA, aB, max: Math.max(sync, bA, aB) };
776
+ const sync = cosineFromOverlap(kSync, densityA, densityB);
777
+ const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
778
+ const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
779
+ const max = Math.max(sync, bA, aB);
780
+ const windowSize = entryA.meta.buffer.length;
781
+ const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
782
+ const kLead = Math.max(kALeadsB, kBLeadsA);
783
+ const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
784
+ return {
785
+ sync,
786
+ bA,
787
+ aB,
788
+ max,
789
+ kSync,
790
+ kALeadsB,
791
+ kBLeadsA,
792
+ densityA,
793
+ densityB,
794
+ significantSync,
795
+ significantLead
796
+ };
707
797
  };
798
+ var overlapFrom = (s) => ({
799
+ kSync: s.kSync,
800
+ densityA: s.densityA,
801
+ densityB: s.densityB,
802
+ cosine: s.sync
803
+ });
708
804
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
709
805
  if (entryA.label === entryB.label) return true;
710
806
  if (isSameField(entryA.label, entryB.label)) return true;
@@ -728,52 +824,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
728
824
  const roleA = entryA.meta.role;
729
825
  const roleB = entryB.meta.role;
730
826
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
731
- if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
827
+ if (similarities.densityA < 2 || similarities.densityB < 2) return;
828
+ const overlap = overlapFrom(similarities);
732
829
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
733
830
  redundantSet.add(entryA.label);
734
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
735
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
831
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, overlap });
832
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
736
833
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
737
834
  redundantSet.add(entryB.label);
738
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
739
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
835
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, overlap });
836
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, overlap);
740
837
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
741
838
  redundantSet.add(entryA.label);
742
839
  redundantSet.add(entryB.label);
743
- pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
744
- pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
745
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
840
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, overlap });
841
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, overlap });
842
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
746
843
  }
747
844
  };
748
845
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
749
846
  if (entryA.isVolatile || entryB.isVolatile) return;
750
- if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
751
847
  const addLeak = (source, target) => {
752
848
  if (isEventDriven(target, graph)) return;
753
- if (!violationMap.has(source)) {
754
- violationMap.set(source, []);
755
- }
756
- violationMap.get(source).push({ type: "causal_leak", target });
849
+ pushViolation(violationMap, source, { type: "causal_leak", target });
757
850
  const sourceEntry = source === entryA.label ? entryA : entryB;
758
851
  const targetEntry = source === entryA.label ? entryB : entryA;
759
852
  displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
760
853
  };
761
- if (similarities.bA === similarities.max) {
854
+ if (similarities.kALeadsB >= similarities.kBLeadsA) {
762
855
  addLeak(entryA.label, entryB.label);
763
- } else if (similarities.aB === similarities.max) {
856
+ } else {
764
857
  addLeak(entryB.label, entryA.label);
765
858
  }
766
859
  };
767
860
  var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
768
- let compCount = 0;
769
861
  const violationMap = /* @__PURE__ */ new Map();
862
+ let compCount = 0;
770
863
  for (const entryA of dirtyEntries) {
771
864
  for (const entryB of allEntries) {
772
865
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
773
866
  compCount++;
774
867
  const similarities = calculateAllSimilarities(entryA, entryB);
775
- if (similarities.max > SIMILARITY_THRESHOLD) {
868
+ if (similarities.significantSync) {
776
869
  detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
870
+ }
871
+ if (similarities.significantLead) {
777
872
  detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
778
873
  }
779
874
  }
@@ -1032,9 +1127,9 @@ var beginEffectTracking = (l) => {
1032
1127
  var endEffectTracking = () => {
1033
1128
  instance.currentEffectSource = null;
1034
1129
  };
1035
- var printBasisHealthReport = (threshold = 0.5) => {
1130
+ var printBasisHealthReport = () => {
1036
1131
  if (!instance.config.debug) return;
1037
- displayHealthReport(instance.history, threshold, instance.violationMap);
1132
+ displayHealthReport(instance.history, instance.violationMap);
1038
1133
  };
1039
1134
  var getBasisMetrics = () => ({
1040
1135
  engine: "v0.6.x",