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.
@@ -24,33 +24,92 @@ __export(zustand_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(zustand_exports);
26
26
 
27
+ // src/core/constants.ts
28
+ var WINDOW_SIZE = 50;
29
+ var PAIR_RARITY_TARGET = 0.01;
30
+ var RELATIVE_OVERLAP_FLOOR = 0.65;
31
+ var LOOP_THRESHOLD = 150;
32
+ var VOLATILITY_THRESHOLD = 25;
33
+ var INSTANCE_SEP = "##";
34
+
27
35
  // src/core/math.ts
28
- var calculateSimilarityCircular = (bufferA, headA, bufferB, headB, offset) => {
36
+ var choose = (n, k) => {
37
+ if (k < 0 || k > n) return 0;
38
+ k = Math.min(k, n - k);
39
+ let result = 1;
40
+ for (let i = 0; i < k; i++) {
41
+ result = result * (n - i) / (i + 1);
42
+ }
43
+ return result;
44
+ };
45
+ var hypergeomPMF = (k, n1, m1, N) => {
46
+ const total = choose(N, n1);
47
+ if (total === 0) return 0;
48
+ return choose(m1, k) * choose(N - m1, n1 - k) / total;
49
+ };
50
+ var hypergeomUpperTailP = (kStart, n1, m1, N) => {
51
+ const kMax = Math.min(n1, m1);
52
+ let p = 0;
53
+ for (let k = Math.max(0, kStart); k <= kMax; k++) {
54
+ p += hypergeomPMF(k, n1, m1, N);
55
+ }
56
+ return p;
57
+ };
58
+ var minOverlapForRarity = (n1, m1, N, targetP) => {
59
+ const kMax = Math.min(n1, m1);
60
+ for (let k = 0; k <= kMax; k++) {
61
+ if (hypergeomUpperTailP(k, n1, m1, N) <= targetP) return k;
62
+ }
63
+ return kMax + 1;
64
+ };
65
+ var overlapThresholdCache = /* @__PURE__ */ new Map();
66
+ var getMinOverlap = (densityA, densityB, windowSize) => {
67
+ const lo = Math.min(densityA, densityB) | 0;
68
+ const hi = Math.max(densityA, densityB) | 0;
69
+ const key = `${lo}_${hi}_${windowSize}`;
70
+ let cached = overlapThresholdCache.get(key);
71
+ if (cached === void 0) {
72
+ const rare = minOverlapForRarity(lo, hi, windowSize, PAIR_RARITY_TARGET);
73
+ const floor = Math.ceil(RELATIVE_OVERLAP_FLOOR * lo);
74
+ cached = Math.max(rare, floor);
75
+ overlapThresholdCache.set(key, cached);
76
+ }
77
+ return cached;
78
+ };
79
+ var isSignificantOverlap = (overlap, densityA, densityB, windowSize) => {
80
+ if (densityA < 2 || densityB < 2) return false;
81
+ return overlap >= getMinOverlap(densityA, densityB, windowSize);
82
+ };
83
+ var countOverlapsCircular = (bufferA, headA, bufferB, headB) => {
29
84
  const L = bufferA.length;
30
- let dot = 0, magA = 0, magB = 0;
31
- const baseOffset = ((headB - headA + offset) % L + L) % L;
85
+ const offSync = ((headB - headA) % L + L) % L;
86
+ const offALeadsB = ((headB - headA + 1) % L + L) % L;
87
+ const offBLeadsA = ((headB - headA - 1) % L + L) % L;
88
+ let kSync = 0;
89
+ let kALeadsB = 0;
90
+ let kBLeadsA = 0;
91
+ let densityA = 0;
92
+ let densityB = 0;
32
93
  for (let i = 0; i < L; i++) {
33
- const valA = bufferA[i];
34
- let iB = i + baseOffset;
35
- if (iB >= L) {
36
- iB -= L;
37
- }
38
- const valB = bufferB[iB];
39
- dot += valA * valB;
40
- magA += valA * valA;
41
- magB += valB * valB;
94
+ const a = bufferA[i] ? 1 : 0;
95
+ const b = bufferB[i] ? 1 : 0;
96
+ densityA += a;
97
+ densityB += b;
98
+ let iSync = i + offSync;
99
+ if (iSync >= L) iSync -= L;
100
+ let iALeadsB = i + offALeadsB;
101
+ if (iALeadsB >= L) iALeadsB -= L;
102
+ let iBLeadsA = i + offBLeadsA;
103
+ if (iBLeadsA >= L) iBLeadsA -= L;
104
+ if (a && bufferB[iSync]) kSync++;
105
+ if (a && bufferB[iALeadsB]) kALeadsB++;
106
+ if (a && bufferB[iBLeadsA]) kBLeadsA++;
42
107
  }
43
- if (magA === 0 || magB === 0) return 0;
44
- return dot / (Math.sqrt(magA) * Math.sqrt(magB));
108
+ return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
45
109
  };
46
- var calculateCosineSimilarity = (A, B) => {
47
- let dot = 0, magA = 0, magB = 0;
48
- for (let i = 0; i < A.length; i++) {
49
- dot += A[i] * B[i];
50
- magA += A[i] * A[i];
51
- magB += B[i] * B[i];
52
- }
53
- return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
110
+ var cosineFromOverlap = (overlap, densityA, densityB) => {
111
+ if (densityA <= 0 || densityB <= 0) return 0;
112
+ return overlap / Math.sqrt(densityA * densityB);
54
113
  };
55
114
 
56
115
  // src/core/graph.ts
@@ -109,13 +168,6 @@ var groupEventSources = (nodes, edges) => {
109
168
  return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
110
169
  };
111
170
 
112
- // src/core/constants.ts
113
- var WINDOW_SIZE = 50;
114
- var SIMILARITY_THRESHOLD = 0.88;
115
- var LOOP_THRESHOLD = 150;
116
- var VOLATILITY_THRESHOLD = 25;
117
- var INSTANCE_SEP = "##";
118
-
119
171
  // src/core/label.ts
120
172
  var stripInstance = (label) => {
121
173
  const idx = label.indexOf(INSTANCE_SEP);
@@ -229,32 +281,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
229
281
  var LOG_COOLDOWN = 3e3;
230
282
  var THEME = {
231
283
  identity: "#6C5CE7",
232
- // Purple (Brand)
233
284
  problem: "#D63031",
234
- // Red (Bugs)
235
285
  solution: "#FBC531",
236
- // Yellow (Fixes)
237
286
  context: "#0984E3",
238
- // Blue (Locations)
239
287
  muted: "#9AA0A6",
240
- // Gray (Metadata)
241
288
  border: "#2E2E35",
242
289
  success: "#00b894"
243
- // Green (Good Score)
244
290
  };
245
291
  var STYLES = {
246
- // Structure
247
292
  basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
248
293
  headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
249
294
  headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
250
295
  version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
251
- // Actions
252
296
  actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
253
297
  actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
254
- // Context
255
298
  impactLabel: `color: ${THEME.context}; font-weight: bold;`,
256
299
  location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
257
- // Text
258
300
  subText: `color: ${THEME.muted}; font-size: 11px;`,
259
301
  bold: "font-weight: bold;",
260
302
  label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
@@ -269,97 +311,95 @@ var shouldLog = (key) => {
269
311
  return false;
270
312
  };
271
313
  var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
314
+ var displayName = (raw) => {
315
+ const { name } = parseLabel(raw);
316
+ return name.replace(/:\d+$/, "");
317
+ };
318
+ var areSyncSignificant = (metaA, metaB) => {
319
+ const { kSync, densityA, densityB } = countOverlapsCircular(
320
+ metaA.buffer,
321
+ metaA.head,
322
+ metaB.buffer,
323
+ metaB.head
324
+ );
325
+ return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
326
+ };
272
327
  var getSuggestedFix = (issue, info) => {
273
328
  if (issue.label.includes("Global Event")) {
274
- return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
329
+ 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.`;
275
330
  }
276
331
  const violations = issue.violations || [];
277
332
  const leaks = violations.filter((v) => v.type === "causal_leak");
278
333
  const mirrors = violations.filter((v) => v.type === "context_mirror");
279
334
  const duplicates = violations.filter((v) => v.type === "duplicate_state");
280
335
  if (mirrors.length > 0) {
281
- return `Local state is 'shadowing' Global Context. This creates two sources of truth. Delete the local state and consume the %cContext%c value directly.`;
336
+ 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.`;
282
337
  }
283
338
  if (leaks.length > 0) {
284
- const targetName = parseLabel(leaks[0].target).name;
339
+ const targetName = displayName(leaks[0].target);
285
340
  if (issue.label.includes("effect")) {
286
- 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.`;
341
+ return `An effect is calling setState on ${targetName}, which paints again. If you can compute ${targetName} while rendering, drop the %ceffect%c.`;
287
342
  }
288
- return `State cascading detected. ${info.name} triggers ${targetName} in a separate frame. Merge them into one object to update simultaneously.`;
343
+ return `${info.name} updates, then ${targetName} updates on the next frame. If they are one fact, write them in the same %csetState%c.`;
289
344
  }
290
345
  if (duplicates.length > 0) {
291
346
  if (isBooleanLike(info.name)) {
292
- return `Boolean Explosion detected. Multiple flags are toggling in sync. Replace impossible states with a single %cstatus%c string ('idle' | 'loading' | 'success').`;
347
+ return `Several flags move together. One %cstatus%c value avoids impossible combinations.`;
293
348
  }
294
- 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.`;
349
+ return `These hooks move together. If one is just the other in another shape, compute it while %crendering%c.`;
295
350
  }
296
351
  if (issue.metric === "density") {
297
- 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.`;
352
+ 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.`;
298
353
  }
299
- return `Check the dependency chain of ${info.name}.`;
354
+ return `Inspect ${info.name} and what updates with it.`;
300
355
  };
301
- var displayHealthReport = (history2, threshold, violationMap) => {
356
+ var displayHealthReport = (history2, violationMap) => {
302
357
  if (!isWeb) return;
303
358
  const entries = Array.from(history2.entries());
304
359
  if (entries.length === 0) return;
305
360
  const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
306
- console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerIdentity);
361
+ console.group(`%c BASIS | report `, STYLES.headerIdentity);
307
362
  if (topIssues.length > 0) {
308
- console.log(
309
- `%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
310
- `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
311
- `font-weight: normal; color: ${THEME.muted}; font-style: italic;`
312
- );
363
+ console.log(`%cStart here`, `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`);
313
364
  topIssues.forEach((issue, idx) => {
314
365
  const info = parseLabel(issue.label);
315
- const icon = issue.metric === "influence" ? "\u26A1" : "\u{1F4C8}";
366
+ const icon = issue.metric === "influence" ? "\u2192" : "\u2022";
316
367
  const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
317
- let displayName = info.name;
318
- let displayFile = info.file;
319
- if (issue.label.includes("Global Event")) {
320
- displayName = info.name;
321
- displayFile = info.file;
322
- }
323
368
  console.group(
324
- ` %c${idx + 1}%c ${icon} ${displayName} %c(${displayFile})`,
369
+ ` %c${idx + 1}%c ${icon} ${displayName(issue.label)} %c(${info.file})`,
325
370
  `background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
326
371
  "font-family: monospace; font-weight: 700;",
327
- `color: ${THEME.muted}; font-size: 10px; font-weight: normal; font-style: italic;`
372
+ `color: ${THEME.muted}; font-size: 10px; font-weight: normal;`
328
373
  );
329
- console.log(`%c${issue.reason}`, `color: ${THEME.muted}; font-style: italic;`);
374
+ console.log(`%c${issue.reason}`, `color: ${THEME.muted};`);
330
375
  if (issue.violations.length > 0) {
331
376
  const byFile = /* @__PURE__ */ new Map();
332
377
  issue.violations.forEach((v) => {
333
378
  if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
334
379
  const { file, name } = parseLabel(v.target);
335
380
  if (!byFile.has(file)) byFile.set(file, []);
336
- byFile.get(file).push(name);
381
+ byFile.get(file).push(name.replace(/:\d+$/, ""));
337
382
  });
338
383
  const impactParts = [];
339
384
  byFile.forEach((vars, file) => {
340
- const varList = vars.join(", ");
341
- impactParts.push(`${file} (${varList})`);
385
+ impactParts.push(`${file} (${vars.join(", ")})`);
342
386
  });
343
387
  if (impactParts.length > 0) {
344
- console.log(`%cImpacts: %c${impactParts.join(" + ")}`, STYLES.impactLabel, "");
388
+ console.log(`%cAlso updates: %c${impactParts.join(" \xB7 ")}`, STYLES.impactLabel, "");
345
389
  }
346
390
  }
347
391
  const fix = getSuggestedFix(issue, info);
348
392
  const fixParts = fix.split("%c");
349
393
  if (fixParts.length === 3) {
350
394
  console.log(
351
- `%cSolution: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
395
+ `%cTry: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
352
396
  STYLES.actionLabel,
353
397
  "",
354
398
  STYLES.actionPill,
355
399
  ""
356
400
  );
357
401
  } else {
358
- console.log(
359
- `%cSolution: %c${fix}`,
360
- STYLES.actionLabel,
361
- ""
362
- );
402
+ console.log(`%cTry: %c${fix}`, STYLES.actionLabel, "");
363
403
  }
364
404
  console.groupEnd();
365
405
  });
@@ -374,145 +414,179 @@ var displayHealthReport = (history2, threshold, violationMap) => {
374
414
  processed.add(labelA);
375
415
  entries.forEach(([labelB, metaB]) => {
376
416
  if (labelA === labelB || processed.has(labelB)) return;
377
- if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
378
- if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
379
- currentCluster.push(labelB);
380
- processed.add(labelB);
381
- }
417
+ if (!areSyncSignificant(metaA, metaB)) return;
418
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
419
+ currentCluster.push(labelB);
420
+ processed.add(labelB);
382
421
  });
383
422
  if (currentCluster.length > 1) clusters.push(currentCluster);
384
423
  else independentCount++;
385
424
  });
386
425
  const totalVars = entries.length;
387
- const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
388
- let internalEdges = 0;
389
- instance.graph.forEach((targets, source) => {
390
- if (source.startsWith("Event_Tick_")) return;
391
- internalEdges += targets.size;
392
- });
393
- const causalPenalty = internalEdges / totalVars * 100;
394
- let healthScore = redundancyScore - causalPenalty;
395
- if (healthScore < 0) healthScore = 0;
396
- const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
397
426
  console.log(
398
- `%cSystem Efficiency: %c${healthScore.toFixed(1)}%`,
399
- STYLES.bold,
400
- `color: ${scoreColor}; font-weight: bold;`
427
+ `%c${independentCount + clusters.length} of ${totalVars} instrumented hooks look independent in this window.`,
428
+ STYLES.subText
401
429
  );
402
- console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
403
430
  if (clusters.length > 0) {
404
- console.log(`%cDetected ${clusters.length} Sync Issues:`, `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`);
431
+ console.log(
432
+ `%c${clusters.length} group${clusters.length === 1 ? "" : "s"} that keep updating together:`,
433
+ `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`
434
+ );
405
435
  clusters.forEach((cluster, idx) => {
406
436
  const clusterMetas = cluster.map((l) => ({
407
437
  label: l,
408
438
  meta: history2.get(l),
409
- name: parseLabel(l).name
439
+ name: displayName(l)
410
440
  }));
411
441
  const hasCtx = clusterMetas.some(
412
442
  (c) => c.meta.role === "context" /* CONTEXT */ || c.meta.role === "store" /* STORE */
413
443
  );
414
- const names = clusterMetas.map((c) => {
415
- const prefix = c.meta.role === "store" /* STORE */ ? "\u03A3 " : c.meta.role === "context" /* CONTEXT */ ? "\u03A9 " : "";
416
- return `${prefix}${c.name}`;
417
- }).join(" \u27F7 ");
418
- console.group(` %c${idx + 1}%c ${names}`, `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`, "font-family: monospace; font-weight: bold;");
444
+ const names = clusterMetas.map((c) => c.name).join(", ");
445
+ console.group(
446
+ ` %c${idx + 1}%c ${names}`,
447
+ `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`,
448
+ "font-family: monospace; font-weight: bold;"
449
+ );
419
450
  if (hasCtx) {
420
451
  const hasStore = clusterMetas.some((c) => c.meta.role === "store" /* STORE */);
421
- const sourceType = hasStore ? "External Store" : "global context";
422
- console.log(`%cDiagnosis: ${hasStore ? "Store" : "Context"} Mirroring. Local state is shadowing ${sourceType}.`, `color: ${THEME.problem};`);
423
- console.log(`%cSolution: Use ${sourceType} directly to avoid state drift.`, STYLES.actionLabel);
452
+ const sourceType = hasStore ? "a store" : "context";
453
+ console.log(`A local hook is only following ${sourceType}.`);
454
+ console.log(
455
+ `%cTry:%c Read ${sourceType} in render if the local value is not a draft.`,
456
+ STYLES.actionLabel,
457
+ ""
458
+ );
424
459
  } else {
425
- const boolKeywords = ["is", "has", "can", "should", "loading", "success", "error", "active", "enabled", "open", "visible"];
460
+ const boolKeywords = [
461
+ "is",
462
+ "has",
463
+ "can",
464
+ "should",
465
+ "loading",
466
+ "success",
467
+ "error",
468
+ "active",
469
+ "enabled",
470
+ "open",
471
+ "visible"
472
+ ];
426
473
  const boolCount = clusterMetas.filter(
427
474
  (c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
428
475
  ).length;
429
- const isBoolExplosion = cluster.length > 2 && boolCount / cluster.length > 0.5;
430
- if (isBoolExplosion) {
431
- console.log(`%cDiagnosis:%c Boolean Explosion. Multiple booleans updating in sync.`, STYLES.bold, "");
432
- console.log(`%cSolution:%c Combine into a single %cstatus%c string or a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "", STYLES.actionPill, "");
476
+ if (cluster.length > 2 && boolCount / cluster.length > 0.5) {
477
+ console.log(`These flags move together.`);
478
+ console.log(
479
+ `%cTry:%c One %cstatus%c instead of several booleans.`,
480
+ STYLES.actionLabel,
481
+ "",
482
+ STYLES.actionPill,
483
+ ""
484
+ );
433
485
  } else if (cluster.length > 2) {
434
- console.log(`%cDiagnosis:%c Sibling Updates. These states respond to the same event.`, STYLES.bold, "");
435
- console.log(`%cSolution:%c This may be intentional. If not, consolidate into a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
486
+ console.log(`These hooks move on the same frames. Often the same click or fetch.`);
487
+ console.log(
488
+ `%cTry:%c Leave it if that is intentional. Otherwise one %creducer%c.`,
489
+ STYLES.actionLabel,
490
+ "",
491
+ STYLES.actionPill,
492
+ ""
493
+ );
436
494
  } else {
437
- console.log(`%cDiagnosis:%c Redundant State. Variables always change together.`, STYLES.bold, "");
438
- console.log(`%cSolution:%c Derive one from the other via %cuseMemo%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
495
+ console.log(`These two hooks keep updating in the same frame.`);
496
+ console.log(
497
+ `%cTry:%c If one is derived, compute it while %crendering%c.`,
498
+ STYLES.actionLabel,
499
+ "",
500
+ STYLES.actionPill,
501
+ ""
502
+ );
439
503
  }
440
504
  }
441
505
  console.groupEnd();
442
506
  });
443
507
  } else {
444
- console.log("%c\u2728 Your architecture is clean. No redundant state detected.", `color: ${THEME.success}; font-weight: bold;`);
508
+ console.log(
509
+ "%cNo hooks were updating in lockstep in this window.",
510
+ `color: ${THEME.success}; font-weight: bold;`
511
+ );
445
512
  }
446
513
  console.groupEnd();
447
514
  };
448
- var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
515
+ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, overlap) => {
449
516
  if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
450
517
  const infoA = parseLabel(labelA);
451
- const infoB = parseLabel(labelB);
518
+ const nameA = displayName(labelA);
519
+ const nameB = displayName(labelB);
452
520
  const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
453
521
  const isStoreMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "store" /* STORE */ || metaB.role === "local" /* LOCAL */ && metaA.role === "store" /* STORE */;
454
- const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
455
- console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
456
- console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
457
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, STYLES.bold, "");
522
+ const alertType = isContextMirror ? "local state follows context" : isStoreMirror ? "local state follows a store" : "hooks moving together";
523
+ const times = overlap.kSync === 1 ? "time" : "times";
524
+ console.group(`%c BASIS | ${alertType} `, STYLES.headerProblem);
525
+ console.log(`%c${infoA.file}`, STYLES.location);
526
+ console.log(
527
+ `%c${nameA}%c and %c${nameB}%c updated in the same frame ${overlap.kSync} ${times}.`,
528
+ STYLES.label,
529
+ "",
530
+ STYLES.label,
531
+ ""
532
+ );
458
533
  if (isContextMirror || isStoreMirror) {
459
- const sourceType = isStoreMirror ? "External Store" : "Global Context";
534
+ const sourceType = isStoreMirror ? "store" : "context";
535
+ console.log(
536
+ `%cTry:%c If this is not a draft, delete the local hook and read the %c${sourceType}%c in render.`,
537
+ STYLES.bold,
538
+ "",
539
+ STYLES.actionPill,
540
+ ""
541
+ );
542
+ } else if (isBooleanLike(nameA) || isBooleanLike(nameB)) {
460
543
  console.log(
461
- `%cFix:%c Local state is 'shadowing' ${sourceType}. Delete the local state and consume the %c${sourceType}%c value directly.`,
544
+ `%cTry:%c One %cstatus%c instead of several flags.`,
462
545
  STYLES.bold,
463
546
  "",
464
547
  STYLES.actionPill,
465
548
  ""
466
549
  );
467
550
  } else {
468
- if (isBooleanLike(infoA.name) || isBooleanLike(infoB.name)) {
469
- console.log(
470
- `%cFix:%c Boolean Explosion detected. Merge flags into a single %cstatus%c string or %cuseReducer%c.`,
471
- STYLES.bold,
472
- "",
473
- STYLES.actionPill,
474
- "",
475
- STYLES.actionPill,
476
- ""
477
- );
478
- } else {
479
- console.log(
480
- `%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
481
- STYLES.bold,
482
- "",
483
- STYLES.label,
484
- "",
485
- STYLES.label,
486
- "",
487
- STYLES.actionPill,
488
- ""
489
- );
490
- }
551
+ console.log(
552
+ `%cTry:%c If %c${nameB}%c is just %c${nameA}%c in another shape, compute it while rendering.`,
553
+ STYLES.bold,
554
+ "",
555
+ STYLES.label,
556
+ "",
557
+ STYLES.label,
558
+ ""
559
+ );
491
560
  }
492
561
  console.groupEnd();
493
562
  };
494
- var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
563
+ var displayCausalHint = (targetLabel, _targetMeta, sourceLabel, sourceMeta) => {
495
564
  if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
496
565
  const target = parseLabel(targetLabel);
497
- const source = parseLabel(sourceLabel);
498
- const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "CONTEXT SYNC LEAK" : sourceMeta.role === "store" /* STORE */ ? "STORE SYNC LEAK" : "DOUBLE RENDER";
566
+ const sourceName = displayName(sourceLabel);
567
+ const targetName = displayName(targetLabel);
568
+ const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "extra render from context" : sourceMeta.role === "store" /* STORE */ ? "extra render from a store" : "extra render";
499
569
  const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
500
- console.groupCollapsed(`%c \u26A1 BASIS | ${headerType} `, STYLES.headerProblem);
501
- console.log(`%c\u{1F4CD} Location: %c${target.file}`, STYLES.bold, STYLES.location);
502
- console.log(`%cIssue:%c ${source.name} triggers ${target.name} in separate frames.`, STYLES.bold, "");
570
+ console.groupCollapsed(`%c BASIS | ${headerType} `, STYLES.headerProblem);
571
+ console.log(`%c${target.file}`, STYLES.location);
572
+ console.log(
573
+ `%c${sourceName}%c updates %c${targetName}%c on the next frame.`,
574
+ STYLES.label,
575
+ "",
576
+ STYLES.label,
577
+ ""
578
+ );
503
579
  if (isEffect) {
504
580
  console.log(
505
- `%cFix:%c Derive %c${target.name}%c during the render phase (remove effect) or wrap in %cuseMemo%c.`,
581
+ `%cTry:%c If %c${targetName}%c can be computed while rendering, drop the extra setState.`,
506
582
  STYLES.bold,
507
583
  "",
508
584
  STYLES.label,
509
- "",
510
- STYLES.actionPill,
511
585
  ""
512
586
  );
513
587
  } else {
514
588
  console.log(
515
- `%cFix:%c Merge %c${target.name}%c with %c${source.name}%c into a single state update.`,
589
+ `%cTry:%c Write %c${targetName}%c in the same update as %c${sourceName}%c if they are one fact.`,
516
590
  STYLES.bold,
517
591
  "",
518
592
  STYLES.label,
@@ -545,7 +619,7 @@ var displayGraphReport = (graph) => {
545
619
  if (!isWeb) return;
546
620
  if (graph.nodes.length === 0) {
547
621
  console.log(
548
- `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
622
+ `%c BASIS | update graph %c(nothing recorded yet)`,
549
623
  STYLES.headerIdentity,
550
624
  `color: ${THEME.muted}; font-style: italic;`
551
625
  );
@@ -564,17 +638,22 @@ var displayGraphReport = (graph) => {
564
638
  occurrences: g.occurrences
565
639
  }));
566
640
  const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
567
- 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 }));
641
+ const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({
642
+ sourceIds: [id],
643
+ sourceNode: nodeById.get(id),
644
+ edges: outgoing.get(id),
645
+ occurrences: 1
646
+ }));
568
647
  const groups = [...eventGroups, ...nonEventGroups].sort(
569
648
  (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
570
649
  );
571
650
  console.group(
572
- `%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}`,
651
+ `%c BASIS | update graph %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 last ${graph.bufferWindowSize} frames`,
573
652
  STYLES.headerIdentity,
574
- `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
653
+ `color: ${THEME.muted}; font-weight: normal;`
575
654
  );
576
655
  console.log(
577
- `%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
656
+ `%cparent \u2192 child = what we saw cause an update. (\xD7N) = times in this window. Repeat clicks with the same targets are grouped.`,
578
657
  STYLES.subText
579
658
  );
580
659
  groups.forEach((group) => {
@@ -582,12 +661,12 @@ var displayGraphReport = (graph) => {
582
661
  const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
583
662
  const isFx = group.sourceNode?.role === "effect";
584
663
  const isUnknown = group.sourceNode?.role === "unknown";
585
- const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
664
+ const icon = isEvent ? "\u2022" : isCtx ? "ctx" : isFx ? "fx" : isUnknown ? "?" : "\u2022";
586
665
  const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
587
666
  const fanout = group.edges.length;
588
667
  const hits = group.occurrences;
589
668
  const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
590
- const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
669
+ const title = isEvent ? `click / event \xB7 ${fanout} update${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
591
670
  console.groupCollapsed(
592
671
  `%c${icon} %c${title}`,
593
672
  `color: ${color};`,
@@ -599,16 +678,16 @@ var displayGraphReport = (graph) => {
599
678
  const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
600
679
  if (target?.redundant) {
601
680
  console.log(
602
- `%c ${label}%c${weight} %credundant`,
681
+ `%c ${label}%c${weight} %cmoving with another hook`,
603
682
  `color: ${THEME.muted}; font-family: monospace;`,
604
- `color: ${THEME.muted}; font-style: italic;`,
683
+ `color: ${THEME.muted};`,
605
684
  `color: ${THEME.problem}; font-weight: bold;`
606
685
  );
607
686
  } else {
608
687
  console.log(
609
688
  `%c ${label}%c${weight}`,
610
689
  `color: ${THEME.muted}; font-family: monospace;`,
611
- `color: ${THEME.muted}; font-style: italic;`
690
+ `color: ${THEME.muted};`
612
691
  );
613
692
  }
614
693
  });
@@ -616,19 +695,21 @@ var displayGraphReport = (graph) => {
616
695
  });
617
696
  console.groupEnd();
618
697
  };
619
- var displayViolentBreaker = (label, count, threshold) => {
698
+ var displayViolentBreaker = (label, count, _threshold) => {
620
699
  if (!isWeb) return;
621
- const { name } = parseLabel(label);
622
- console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, STYLES.headerProblem);
623
- console.error(`INFINITE LOOP DETECTED
624
- Variable: ${name}
625
- Frequency: ${count} updates/sec`);
626
- console.log(`%cACTION: Update BLOCKED to prevent browser freeze.`, `color: ${THEME.problem}; font-weight: bold;`);
700
+ const name = displayName(label);
701
+ console.group(`%c BASIS | loop guard `, STYLES.headerProblem);
702
+ console.error(
703
+ `${name} updated ${count} times in one second. Basis stopped recording this path so the tab stays usable.`
704
+ );
705
+ console.log(
706
+ `%cReact may still error on its own. Fix the effect that writes a value it also lists as a dependency.`,
707
+ `color: ${THEME.muted};`
708
+ );
627
709
  console.groupEnd();
628
710
  };
629
711
 
630
712
  // src/core/analysis.ts
631
- var CAUSAL_MARGIN = 0.05;
632
713
  var isEventDriven = (label, graph) => {
633
714
  for (const [parent, targets] of graph.entries()) {
634
715
  if (parent.startsWith("Event_Tick_") && targets.has(label)) {
@@ -638,29 +719,40 @@ var isEventDriven = (label, graph) => {
638
719
  return false;
639
720
  };
640
721
  var calculateAllSimilarities = (entryA, entryB) => {
641
- const sync = calculateSimilarityCircular(
642
- entryA.meta.buffer,
643
- entryA.meta.head,
644
- entryB.meta.buffer,
645
- entryB.meta.head,
646
- 0
647
- );
648
- const bA = calculateSimilarityCircular(
649
- entryA.meta.buffer,
650
- entryA.meta.head,
651
- entryB.meta.buffer,
652
- entryB.meta.head,
653
- 1
654
- );
655
- const aB = calculateSimilarityCircular(
722
+ const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
656
723
  entryA.meta.buffer,
657
724
  entryA.meta.head,
658
725
  entryB.meta.buffer,
659
- entryB.meta.head,
660
- -1
726
+ entryB.meta.head
661
727
  );
662
- return { sync, bA, aB, max: Math.max(sync, bA, aB) };
728
+ const sync = cosineFromOverlap(kSync, densityA, densityB);
729
+ const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
730
+ const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
731
+ const max = Math.max(sync, bA, aB);
732
+ const windowSize = entryA.meta.buffer.length;
733
+ const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
734
+ const kLead = Math.max(kALeadsB, kBLeadsA);
735
+ const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
736
+ return {
737
+ sync,
738
+ bA,
739
+ aB,
740
+ max,
741
+ kSync,
742
+ kALeadsB,
743
+ kBLeadsA,
744
+ densityA,
745
+ densityB,
746
+ significantSync,
747
+ significantLead
748
+ };
663
749
  };
750
+ var overlapFrom = (s) => ({
751
+ kSync: s.kSync,
752
+ densityA: s.densityA,
753
+ densityB: s.densityB,
754
+ cosine: s.sync
755
+ });
664
756
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
665
757
  if (entryA.label === entryB.label) return true;
666
758
  if (isSameField(entryA.label, entryB.label)) return true;
@@ -684,52 +776,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
684
776
  const roleA = entryA.meta.role;
685
777
  const roleB = entryB.meta.role;
686
778
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
687
- if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
779
+ if (similarities.densityA < 2 || similarities.densityB < 2) return;
780
+ const overlap = overlapFrom(similarities);
688
781
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
689
782
  redundantSet.add(entryA.label);
690
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
691
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
783
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, overlap });
784
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
692
785
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
693
786
  redundantSet.add(entryB.label);
694
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
695
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
787
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, overlap });
788
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, overlap);
696
789
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
697
790
  redundantSet.add(entryA.label);
698
791
  redundantSet.add(entryB.label);
699
- pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
700
- pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
701
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
792
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, overlap });
793
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, overlap });
794
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
702
795
  }
703
796
  };
704
797
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
705
798
  if (entryA.isVolatile || entryB.isVolatile) return;
706
- if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
707
799
  const addLeak = (source, target) => {
708
800
  if (isEventDriven(target, graph)) return;
709
- if (!violationMap.has(source)) {
710
- violationMap.set(source, []);
711
- }
712
- violationMap.get(source).push({ type: "causal_leak", target });
801
+ pushViolation(violationMap, source, { type: "causal_leak", target });
713
802
  const sourceEntry = source === entryA.label ? entryA : entryB;
714
803
  const targetEntry = source === entryA.label ? entryB : entryA;
715
804
  displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
716
805
  };
717
- if (similarities.bA === similarities.max) {
806
+ if (similarities.kALeadsB >= similarities.kBLeadsA) {
718
807
  addLeak(entryA.label, entryB.label);
719
- } else if (similarities.aB === similarities.max) {
808
+ } else {
720
809
  addLeak(entryB.label, entryA.label);
721
810
  }
722
811
  };
723
812
  var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
724
- let compCount = 0;
725
813
  const violationMap = /* @__PURE__ */ new Map();
814
+ let compCount = 0;
726
815
  for (const entryA of dirtyEntries) {
727
816
  for (const entryB of allEntries) {
728
817
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
729
818
  compCount++;
730
819
  const similarities = calculateAllSimilarities(entryA, entryB);
731
- if (similarities.max > SIMILARITY_THRESHOLD) {
820
+ if (similarities.significantSync) {
732
821
  detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
822
+ }
823
+ if (similarities.significantLead) {
733
824
  detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
734
825
  }
735
826
  }
@@ -962,9 +1053,9 @@ var registerVariable = (l, o = {}) => {
962
1053
  });
963
1054
  }
964
1055
  };
965
- var printBasisHealthReport = (threshold = 0.5) => {
1056
+ var printBasisHealthReport = () => {
966
1057
  if (!instance.config.debug) return;
967
- displayHealthReport(instance.history, threshold, instance.violationMap);
1058
+ displayHealthReport(instance.history, instance.violationMap);
968
1059
  };
969
1060
  var getBasisMetrics = () => ({
970
1061
  engine: "v0.6.x",