react-state-basis 0.6.6 → 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/README.md CHANGED
@@ -168,7 +168,7 @@ A detected pattern is **not automatically a bug**, and Basis does not know the i
168
168
 
169
169
  Use the results as prompts for investigation rather than as rules for how React code should be written.
170
170
 
171
- [See examples and possible fixes →](https://github.com/liovic/react-state-basis/wiki/The-Forensic-Catalog)
171
+ [See examples and possible fixes →](https://github.com/liovic/react-state-basis/wiki/Detected-patterns)
172
172
 
173
173
  ---
174
174
 
@@ -338,7 +338,7 @@ Basis is designed primarily as a development-time diagnostic tool.
338
338
 
339
339
  Actual overhead depends on the application and instrumentation configuration.
340
340
 
341
- [See benchmarks →](https://github.com/liovic/react-state-basis/wiki/Performance-Forensics)
341
+ [See benchmarks →](https://github.com/liovic/react-state-basis/wiki/Performance)
342
342
 
343
343
  ---
344
344
 
@@ -285,6 +285,10 @@ var shouldLog = (key) => {
285
285
  return false;
286
286
  };
287
287
  var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
288
+ var displayName = (raw) => {
289
+ const { name } = parseLabel(raw);
290
+ return name.replace(/:\d+$/, "");
291
+ };
288
292
  var areSyncSignificant = (metaA, metaB) => {
289
293
  const { kSync, densityA, densityB } = countOverlapsCircular(
290
294
  metaA.buffer,
@@ -296,95 +300,80 @@ var areSyncSignificant = (metaA, metaB) => {
296
300
  };
297
301
  var getSuggestedFix = (issue, info) => {
298
302
  if (issue.label.includes("Global Event")) {
299
- return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
303
+ 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.`;
300
304
  }
301
305
  const violations = issue.violations || [];
302
306
  const leaks = violations.filter((v) => v.type === "causal_leak");
303
307
  const mirrors = violations.filter((v) => v.type === "context_mirror");
304
308
  const duplicates = violations.filter((v) => v.type === "duplicate_state");
305
309
  if (mirrors.length > 0) {
306
- return `Local state is 'shadowing' Global Context. This creates two sources of truth. Delete the local state and consume the %cContext%c value directly.`;
310
+ 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.`;
307
311
  }
308
312
  if (leaks.length > 0) {
309
- const targetName = parseLabel(leaks[0].target).name;
313
+ const targetName = displayName(leaks[0].target);
310
314
  if (issue.label.includes("effect")) {
311
- 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.`;
315
+ return `An effect is calling setState on ${targetName}, which paints again. If you can compute ${targetName} while rendering, drop the %ceffect%c.`;
312
316
  }
313
- return `State cascading detected. ${info.name} triggers ${targetName} in a separate frame. Merge them into one object to update simultaneously.`;
317
+ return `${info.name} updates, then ${targetName} updates on the next frame. If they are one fact, write them in the same %csetState%c.`;
314
318
  }
315
319
  if (duplicates.length > 0) {
316
320
  if (isBooleanLike(info.name)) {
317
- return `Boolean Explosion detected. Multiple flags are toggling in sync. Replace impossible states with a single %cstatus%c string ('idle' | 'loading' | 'success').`;
321
+ return `Several flags move together. One %cstatus%c value avoids impossible combinations.`;
318
322
  }
319
- 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.`;
323
+ return `These hooks move together. If one is just the other in another shape, compute it while %crendering%c.`;
320
324
  }
321
325
  if (issue.metric === "density") {
322
- 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.`;
326
+ 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.`;
323
327
  }
324
- return `Check the dependency chain of ${info.name}.`;
328
+ return `Inspect ${info.name} and what updates with it.`;
325
329
  };
326
330
  var displayHealthReport = (history2, violationMap) => {
327
331
  if (!isWeb) return;
328
332
  const entries = Array.from(history2.entries());
329
333
  if (entries.length === 0) return;
330
334
  const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
331
- console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerIdentity);
335
+ console.group(`%c BASIS | report `, STYLES.headerIdentity);
332
336
  if (topIssues.length > 0) {
333
- console.log(
334
- `%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
335
- `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
336
- `font-weight: normal; color: ${THEME.muted}; font-style: italic;`
337
- );
337
+ console.log(`%cStart here`, `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`);
338
338
  topIssues.forEach((issue, idx) => {
339
339
  const info = parseLabel(issue.label);
340
- const icon = issue.metric === "influence" ? "\u26A1" : "\u{1F4C8}";
340
+ const icon = issue.metric === "influence" ? "\u2192" : "\u2022";
341
341
  const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
342
- let displayName = info.name;
343
- let displayFile = info.file;
344
- if (issue.label.includes("Global Event")) {
345
- displayName = info.name;
346
- displayFile = info.file;
347
- }
348
342
  console.group(
349
- ` %c${idx + 1}%c ${icon} ${displayName} %c(${displayFile})`,
343
+ ` %c${idx + 1}%c ${icon} ${displayName(issue.label)} %c(${info.file})`,
350
344
  `background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
351
345
  "font-family: monospace; font-weight: 700;",
352
- `color: ${THEME.muted}; font-size: 10px; font-weight: normal; font-style: italic;`
346
+ `color: ${THEME.muted}; font-size: 10px; font-weight: normal;`
353
347
  );
354
- console.log(`%c${issue.reason}`, `color: ${THEME.muted}; font-style: italic;`);
348
+ console.log(`%c${issue.reason}`, `color: ${THEME.muted};`);
355
349
  if (issue.violations.length > 0) {
356
350
  const byFile = /* @__PURE__ */ new Map();
357
351
  issue.violations.forEach((v) => {
358
352
  if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
359
353
  const { file, name } = parseLabel(v.target);
360
354
  if (!byFile.has(file)) byFile.set(file, []);
361
- byFile.get(file).push(name);
355
+ byFile.get(file).push(name.replace(/:\d+$/, ""));
362
356
  });
363
357
  const impactParts = [];
364
358
  byFile.forEach((vars, file) => {
365
- const varList = vars.join(", ");
366
- impactParts.push(`${file} (${varList})`);
359
+ impactParts.push(`${file} (${vars.join(", ")})`);
367
360
  });
368
361
  if (impactParts.length > 0) {
369
- console.log(`%cImpacts: %c${impactParts.join(" + ")}`, STYLES.impactLabel, "");
362
+ console.log(`%cAlso updates: %c${impactParts.join(" \xB7 ")}`, STYLES.impactLabel, "");
370
363
  }
371
364
  }
372
365
  const fix = getSuggestedFix(issue, info);
373
366
  const fixParts = fix.split("%c");
374
367
  if (fixParts.length === 3) {
375
368
  console.log(
376
- `%cSolution: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
369
+ `%cTry: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
377
370
  STYLES.actionLabel,
378
371
  "",
379
372
  STYLES.actionPill,
380
373
  ""
381
374
  );
382
375
  } else {
383
- console.log(
384
- `%cSolution: %c${fix}`,
385
- STYLES.actionLabel,
386
- ""
387
- );
376
+ console.log(`%cTry: %c${fix}`, STYLES.actionLabel, "");
388
377
  }
389
378
  console.groupEnd();
390
379
  });
@@ -408,135 +397,170 @@ var displayHealthReport = (history2, violationMap) => {
408
397
  else independentCount++;
409
398
  });
410
399
  const totalVars = entries.length;
411
- const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
412
- let internalEdges = 0;
413
- instance.graph.forEach((targets, source) => {
414
- if (source.startsWith("Event_Tick_")) return;
415
- internalEdges += targets.size;
416
- });
417
- const causalPenalty = internalEdges / totalVars * 100;
418
- let healthScore = redundancyScore - causalPenalty;
419
- if (healthScore < 0) healthScore = 0;
420
- const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
421
400
  console.log(
422
- `%cSystem Efficiency: %c${healthScore.toFixed(1)}%`,
423
- STYLES.bold,
424
- `color: ${scoreColor}; font-weight: bold;`
401
+ `%c${independentCount + clusters.length} of ${totalVars} instrumented hooks look independent in this window.`,
402
+ STYLES.subText
425
403
  );
426
- console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
427
404
  if (clusters.length > 0) {
428
- console.log(`%cDetected ${clusters.length} Sync Issues:`, `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`);
405
+ console.log(
406
+ `%c${clusters.length} group${clusters.length === 1 ? "" : "s"} that keep updating together:`,
407
+ `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`
408
+ );
429
409
  clusters.forEach((cluster, idx) => {
430
410
  const clusterMetas = cluster.map((l) => ({
431
411
  label: l,
432
412
  meta: history2.get(l),
433
- name: parseLabel(l).name
413
+ name: displayName(l)
434
414
  }));
435
415
  const hasCtx = clusterMetas.some(
436
416
  (c) => c.meta.role === "context" /* CONTEXT */ || c.meta.role === "store" /* STORE */
437
417
  );
438
- const names = clusterMetas.map((c) => {
439
- const prefix = c.meta.role === "store" /* STORE */ ? "\u03A3 " : c.meta.role === "context" /* CONTEXT */ ? "\u03A9 " : "";
440
- return `${prefix}${c.name}`;
441
- }).join(" \u27F7 ");
442
- console.group(` %c${idx + 1}%c ${names}`, `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`, "font-family: monospace; font-weight: bold;");
418
+ const names = clusterMetas.map((c) => c.name).join(", ");
419
+ console.group(
420
+ ` %c${idx + 1}%c ${names}`,
421
+ `background: ${THEME.problem}; color: white; border-radius: 50%; padding: 0 5px;`,
422
+ "font-family: monospace; font-weight: bold;"
423
+ );
443
424
  if (hasCtx) {
444
425
  const hasStore = clusterMetas.some((c) => c.meta.role === "store" /* STORE */);
445
- const sourceType = hasStore ? "External Store" : "global context";
446
- console.log(`%cDiagnosis: ${hasStore ? "Store" : "Context"} Mirroring. Local state is shadowing ${sourceType}.`, `color: ${THEME.problem};`);
447
- console.log(`%cSolution: Use ${sourceType} directly to avoid state drift.`, STYLES.actionLabel);
426
+ const sourceType = hasStore ? "a store" : "context";
427
+ console.log(`A local hook is only following ${sourceType}.`);
428
+ console.log(
429
+ `%cTry:%c Read ${sourceType} in render if the local value is not a draft.`,
430
+ STYLES.actionLabel,
431
+ ""
432
+ );
448
433
  } else {
449
- const boolKeywords = ["is", "has", "can", "should", "loading", "success", "error", "active", "enabled", "open", "visible"];
434
+ const boolKeywords = [
435
+ "is",
436
+ "has",
437
+ "can",
438
+ "should",
439
+ "loading",
440
+ "success",
441
+ "error",
442
+ "active",
443
+ "enabled",
444
+ "open",
445
+ "visible"
446
+ ];
450
447
  const boolCount = clusterMetas.filter(
451
448
  (c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
452
449
  ).length;
453
- const isBoolExplosion = cluster.length > 2 && boolCount / cluster.length > 0.5;
454
- if (isBoolExplosion) {
455
- console.log(`%cDiagnosis:%c Boolean Explosion. Multiple booleans updating in sync.`, STYLES.bold, "");
456
- console.log(`%cSolution:%c Combine into a single %cstatus%c string or a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "", STYLES.actionPill, "");
450
+ if (cluster.length > 2 && boolCount / cluster.length > 0.5) {
451
+ console.log(`These flags move together.`);
452
+ console.log(
453
+ `%cTry:%c One %cstatus%c instead of several booleans.`,
454
+ STYLES.actionLabel,
455
+ "",
456
+ STYLES.actionPill,
457
+ ""
458
+ );
457
459
  } else if (cluster.length > 2) {
458
- console.log(`%cDiagnosis:%c Sibling Updates. These states respond to the same event.`, STYLES.bold, "");
459
- console.log(`%cSolution:%c This may be intentional. If not, consolidate into a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
460
+ console.log(`These hooks move on the same frames. Often the same click or fetch.`);
461
+ console.log(
462
+ `%cTry:%c Leave it if that is intentional. Otherwise one %creducer%c.`,
463
+ STYLES.actionLabel,
464
+ "",
465
+ STYLES.actionPill,
466
+ ""
467
+ );
460
468
  } else {
461
- console.log(`%cDiagnosis:%c Redundant State. Variables always change together.`, STYLES.bold, "");
462
- console.log(`%cSolution:%c Derive one from the other via %cuseMemo%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
469
+ console.log(`These two hooks keep updating in the same frame.`);
470
+ console.log(
471
+ `%cTry:%c If one is derived, compute it while %crendering%c.`,
472
+ STYLES.actionLabel,
473
+ "",
474
+ STYLES.actionPill,
475
+ ""
476
+ );
463
477
  }
464
478
  }
465
479
  console.groupEnd();
466
480
  });
467
481
  } else {
468
- console.log("%c\u2728 Your architecture is clean. No redundant state detected.", `color: ${THEME.success}; font-weight: bold;`);
482
+ console.log(
483
+ "%cNo hooks were updating in lockstep in this window.",
484
+ `color: ${THEME.success}; font-weight: bold;`
485
+ );
469
486
  }
470
487
  console.groupEnd();
471
488
  };
472
- var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
489
+ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, overlap) => {
473
490
  if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
474
491
  const infoA = parseLabel(labelA);
475
- const infoB = parseLabel(labelB);
492
+ const nameA = displayName(labelA);
493
+ const nameB = displayName(labelB);
476
494
  const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
477
495
  const isStoreMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "store" /* STORE */ || metaB.role === "local" /* LOCAL */ && metaA.role === "store" /* STORE */;
478
- const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
479
- console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
480
- console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
481
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} overlapped on ${(sim * 100).toFixed(0)}% of aligned updates.`, STYLES.bold, "");
496
+ const alertType = isContextMirror ? "local state follows context" : isStoreMirror ? "local state follows a store" : "hooks moving together";
497
+ const times = overlap.kSync === 1 ? "time" : "times";
498
+ console.group(`%c BASIS | ${alertType} `, STYLES.headerProblem);
499
+ console.log(`%c${infoA.file}`, STYLES.location);
500
+ console.log(
501
+ `%c${nameA}%c and %c${nameB}%c updated in the same frame ${overlap.kSync} ${times}.`,
502
+ STYLES.label,
503
+ "",
504
+ STYLES.label,
505
+ ""
506
+ );
482
507
  if (isContextMirror || isStoreMirror) {
483
- const sourceType = isStoreMirror ? "External Store" : "Global Context";
508
+ const sourceType = isStoreMirror ? "store" : "context";
484
509
  console.log(
485
- `%cFix:%c Local state is 'shadowing' ${sourceType}. Delete the local state and consume the %c${sourceType}%c value directly.`,
510
+ `%cTry:%c If this is not a draft, delete the local hook and read the %c${sourceType}%c in render.`,
511
+ STYLES.bold,
512
+ "",
513
+ STYLES.actionPill,
514
+ ""
515
+ );
516
+ } else if (isBooleanLike(nameA) || isBooleanLike(nameB)) {
517
+ console.log(
518
+ `%cTry:%c One %cstatus%c instead of several flags.`,
486
519
  STYLES.bold,
487
520
  "",
488
521
  STYLES.actionPill,
489
522
  ""
490
523
  );
491
524
  } else {
492
- if (isBooleanLike(infoA.name) || isBooleanLike(infoB.name)) {
493
- console.log(
494
- `%cFix:%c Boolean Explosion detected. Merge flags into a single %cstatus%c string or %cuseReducer%c.`,
495
- STYLES.bold,
496
- "",
497
- STYLES.actionPill,
498
- "",
499
- STYLES.actionPill,
500
- ""
501
- );
502
- } else {
503
- console.log(
504
- `%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
505
- STYLES.bold,
506
- "",
507
- STYLES.label,
508
- "",
509
- STYLES.label,
510
- "",
511
- STYLES.actionPill,
512
- ""
513
- );
514
- }
525
+ console.log(
526
+ `%cTry:%c If %c${nameB}%c is just %c${nameA}%c in another shape, compute it while rendering.`,
527
+ STYLES.bold,
528
+ "",
529
+ STYLES.label,
530
+ "",
531
+ STYLES.label,
532
+ ""
533
+ );
515
534
  }
516
535
  console.groupEnd();
517
536
  };
518
- var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
537
+ var displayCausalHint = (targetLabel, _targetMeta, sourceLabel, sourceMeta) => {
519
538
  if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
520
539
  const target = parseLabel(targetLabel);
521
- const source = parseLabel(sourceLabel);
522
- const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "CONTEXT SYNC LEAK" : sourceMeta.role === "store" /* STORE */ ? "STORE SYNC LEAK" : "DOUBLE RENDER";
540
+ const sourceName = displayName(sourceLabel);
541
+ const targetName = displayName(targetLabel);
542
+ const headerType = sourceMeta.role === "context" /* CONTEXT */ ? "extra render from context" : sourceMeta.role === "store" /* STORE */ ? "extra render from a store" : "extra render";
523
543
  const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
524
- console.groupCollapsed(`%c \u26A1 BASIS | ${headerType} `, STYLES.headerProblem);
525
- console.log(`%c\u{1F4CD} Location: %c${target.file}`, STYLES.bold, STYLES.location);
526
- console.log(`%cIssue:%c ${source.name} triggers ${target.name} in separate frames.`, STYLES.bold, "");
544
+ console.groupCollapsed(`%c BASIS | ${headerType} `, STYLES.headerProblem);
545
+ console.log(`%c${target.file}`, STYLES.location);
546
+ console.log(
547
+ `%c${sourceName}%c updates %c${targetName}%c on the next frame.`,
548
+ STYLES.label,
549
+ "",
550
+ STYLES.label,
551
+ ""
552
+ );
527
553
  if (isEffect) {
528
554
  console.log(
529
- `%cFix:%c Derive %c${target.name}%c during the render phase (remove effect) or wrap in %cuseMemo%c.`,
555
+ `%cTry:%c If %c${targetName}%c can be computed while rendering, drop the extra setState.`,
530
556
  STYLES.bold,
531
557
  "",
532
558
  STYLES.label,
533
- "",
534
- STYLES.actionPill,
535
559
  ""
536
560
  );
537
561
  } else {
538
562
  console.log(
539
- `%cFix:%c Merge %c${target.name}%c with %c${source.name}%c into a single state update.`,
563
+ `%cTry:%c Write %c${targetName}%c in the same update as %c${sourceName}%c if they are one fact.`,
540
564
  STYLES.bold,
541
565
  "",
542
566
  STYLES.label,
@@ -569,7 +593,7 @@ var displayGraphReport = (graph) => {
569
593
  if (!isWeb) return;
570
594
  if (graph.nodes.length === 0) {
571
595
  console.log(
572
- `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
596
+ `%c BASIS | update graph %c(nothing recorded yet)`,
573
597
  STYLES.headerIdentity,
574
598
  `color: ${THEME.muted}; font-style: italic;`
575
599
  );
@@ -588,17 +612,22 @@ var displayGraphReport = (graph) => {
588
612
  occurrences: g.occurrences
589
613
  }));
590
614
  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 }));
615
+ const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({
616
+ sourceIds: [id],
617
+ sourceNode: nodeById.get(id),
618
+ edges: outgoing.get(id),
619
+ occurrences: 1
620
+ }));
592
621
  const groups = [...eventGroups, ...nonEventGroups].sort(
593
622
  (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
594
623
  );
595
624
  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}`,
625
+ `%c BASIS | update graph %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 last ${graph.bufferWindowSize} frames`,
597
626
  STYLES.headerIdentity,
598
- `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
627
+ `color: ${THEME.muted}; font-weight: normal;`
599
628
  );
600
629
  console.log(
601
- `%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
630
+ `%cparent \u2192 child = what we saw cause an update. (\xD7N) = times in this window. Repeat clicks with the same targets are grouped.`,
602
631
  STYLES.subText
603
632
  );
604
633
  groups.forEach((group) => {
@@ -606,12 +635,12 @@ var displayGraphReport = (graph) => {
606
635
  const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
607
636
  const isFx = group.sourceNode?.role === "effect";
608
637
  const isUnknown = group.sourceNode?.role === "unknown";
609
- const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
638
+ const icon = isEvent ? "\u2022" : isCtx ? "ctx" : isFx ? "fx" : isUnknown ? "?" : "\u2022";
610
639
  const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
611
640
  const fanout = group.edges.length;
612
641
  const hits = group.occurrences;
613
642
  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]);
643
+ const title = isEvent ? `click / event \xB7 ${fanout} update${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
615
644
  console.groupCollapsed(
616
645
  `%c${icon} %c${title}`,
617
646
  `color: ${color};`,
@@ -623,16 +652,16 @@ var displayGraphReport = (graph) => {
623
652
  const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
624
653
  if (target?.redundant) {
625
654
  console.log(
626
- `%c ${label}%c${weight} %credundant`,
655
+ `%c ${label}%c${weight} %cmoving with another hook`,
627
656
  `color: ${THEME.muted}; font-family: monospace;`,
628
- `color: ${THEME.muted}; font-style: italic;`,
657
+ `color: ${THEME.muted};`,
629
658
  `color: ${THEME.problem}; font-weight: bold;`
630
659
  );
631
660
  } else {
632
661
  console.log(
633
662
  `%c ${label}%c${weight}`,
634
663
  `color: ${THEME.muted}; font-family: monospace;`,
635
- `color: ${THEME.muted}; font-style: italic;`
664
+ `color: ${THEME.muted};`
636
665
  );
637
666
  }
638
667
  });
@@ -640,19 +669,26 @@ var displayGraphReport = (graph) => {
640
669
  });
641
670
  console.groupEnd();
642
671
  };
643
- var displayViolentBreaker = (label, count, threshold) => {
672
+ var displayViolentBreaker = (label, count, _threshold) => {
644
673
  if (!isWeb) return;
645
- const { name } = parseLabel(label);
646
- console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, STYLES.headerProblem);
647
- console.error(`INFINITE LOOP DETECTED
648
- Variable: ${name}
649
- Frequency: ${count} updates/sec`);
650
- console.log(`%cACTION: Update BLOCKED to prevent browser freeze.`, `color: ${THEME.problem}; font-weight: bold;`);
674
+ const name = displayName(label);
675
+ console.group(`%c BASIS | loop guard `, STYLES.headerProblem);
676
+ console.error(
677
+ `${name} updated ${count} times in one second. Basis stopped recording this path so the tab stays usable.`
678
+ );
679
+ console.log(
680
+ `%cReact may still error on its own. Fix the effect that writes a value it also lists as a dependency.`,
681
+ `color: ${THEME.muted};`
682
+ );
651
683
  console.groupEnd();
652
684
  };
653
685
  var displayBootLog = (windowSize) => {
654
686
  if (!isWeb) return;
655
- console.log(`%cBasis%cAuditor%c "Graph Era" (Window: ${windowSize})`, STYLES.basis, STYLES.version, `color: ${THEME.muted}; font-style: italic; margin-left: 8px;`);
687
+ console.log(
688
+ `%cBasis%c watching updates (${windowSize}-frame window)`,
689
+ STYLES.basis,
690
+ `color: ${THEME.muted}; margin-left: 8px;`
691
+ );
656
692
  };
657
693
 
658
694
  // src/core/analysis.ts
@@ -693,6 +729,12 @@ var calculateAllSimilarities = (entryA, entryB) => {
693
729
  significantLead
694
730
  };
695
731
  };
732
+ var overlapFrom = (s) => ({
733
+ kSync: s.kSync,
734
+ densityA: s.densityA,
735
+ densityB: s.densityB,
736
+ cosine: s.sync
737
+ });
696
738
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
697
739
  if (entryA.label === entryB.label) return true;
698
740
  if (isSameField(entryA.label, entryB.label)) return true;
@@ -717,21 +759,21 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
717
759
  const roleB = entryB.meta.role;
718
760
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
719
761
  if (similarities.densityA < 2 || similarities.densityB < 2) return;
720
- const score = similarities.sync;
762
+ const overlap = overlapFrom(similarities);
721
763
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
722
764
  redundantSet.add(entryA.label);
723
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: score });
724
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
765
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, overlap });
766
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
725
767
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
726
768
  redundantSet.add(entryB.label);
727
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: score });
728
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, score);
769
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, overlap });
770
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, overlap);
729
771
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
730
772
  redundantSet.add(entryA.label);
731
773
  redundantSet.add(entryB.label);
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);
774
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, overlap });
775
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, overlap });
776
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
735
777
  }
736
778
  };
737
779
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
@@ -1095,4 +1137,4 @@ export {
1095
1137
  getBasisGraph,
1096
1138
  printBasisGraph
1097
1139
  };
1098
- //# sourceMappingURL=chunk-WMHY2C6D.mjs.map
1140
+ //# sourceMappingURL=chunk-KIXM6YRX.mjs.map