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.
@@ -1,30 +1,89 @@
1
+ // src/core/constants.ts
2
+ var WINDOW_SIZE = 50;
3
+ var PAIR_RARITY_TARGET = 0.01;
4
+ var RELATIVE_OVERLAP_FLOOR = 0.65;
5
+ var LOOP_THRESHOLD = 150;
6
+ var VOLATILITY_THRESHOLD = 25;
7
+ var INSTANCE_SEP = "##";
8
+
1
9
  // src/core/math.ts
2
- var calculateSimilarityCircular = (bufferA, headA, bufferB, headB, offset) => {
10
+ var choose = (n, k) => {
11
+ if (k < 0 || k > n) return 0;
12
+ k = Math.min(k, n - k);
13
+ let result = 1;
14
+ for (let i = 0; i < k; i++) {
15
+ result = result * (n - i) / (i + 1);
16
+ }
17
+ return result;
18
+ };
19
+ var hypergeomPMF = (k, n1, m1, N) => {
20
+ const total = choose(N, n1);
21
+ if (total === 0) return 0;
22
+ return choose(m1, k) * choose(N - m1, n1 - k) / total;
23
+ };
24
+ var hypergeomUpperTailP = (kStart, n1, m1, N) => {
25
+ const kMax = Math.min(n1, m1);
26
+ let p = 0;
27
+ for (let k = Math.max(0, kStart); k <= kMax; k++) {
28
+ p += hypergeomPMF(k, n1, m1, N);
29
+ }
30
+ return p;
31
+ };
32
+ var minOverlapForRarity = (n1, m1, N, targetP) => {
33
+ const kMax = Math.min(n1, m1);
34
+ for (let k = 0; k <= kMax; k++) {
35
+ if (hypergeomUpperTailP(k, n1, m1, N) <= targetP) return k;
36
+ }
37
+ return kMax + 1;
38
+ };
39
+ var overlapThresholdCache = /* @__PURE__ */ new Map();
40
+ var getMinOverlap = (densityA, densityB, windowSize) => {
41
+ const lo = Math.min(densityA, densityB) | 0;
42
+ const hi = Math.max(densityA, densityB) | 0;
43
+ const key = `${lo}_${hi}_${windowSize}`;
44
+ let cached = overlapThresholdCache.get(key);
45
+ if (cached === void 0) {
46
+ const rare = minOverlapForRarity(lo, hi, windowSize, PAIR_RARITY_TARGET);
47
+ const floor = Math.ceil(RELATIVE_OVERLAP_FLOOR * lo);
48
+ cached = Math.max(rare, floor);
49
+ overlapThresholdCache.set(key, cached);
50
+ }
51
+ return cached;
52
+ };
53
+ var isSignificantOverlap = (overlap, densityA, densityB, windowSize) => {
54
+ if (densityA < 2 || densityB < 2) return false;
55
+ return overlap >= getMinOverlap(densityA, densityB, windowSize);
56
+ };
57
+ var countOverlapsCircular = (bufferA, headA, bufferB, headB) => {
3
58
  const L = bufferA.length;
4
- let dot = 0, magA = 0, magB = 0;
5
- const baseOffset = ((headB - headA + offset) % L + L) % L;
59
+ const offSync = ((headB - headA) % L + L) % L;
60
+ const offALeadsB = ((headB - headA + 1) % L + L) % L;
61
+ const offBLeadsA = ((headB - headA - 1) % L + L) % L;
62
+ let kSync = 0;
63
+ let kALeadsB = 0;
64
+ let kBLeadsA = 0;
65
+ let densityA = 0;
66
+ let densityB = 0;
6
67
  for (let i = 0; i < L; i++) {
7
- const valA = bufferA[i];
8
- let iB = i + baseOffset;
9
- if (iB >= L) {
10
- iB -= L;
11
- }
12
- const valB = bufferB[iB];
13
- dot += valA * valB;
14
- magA += valA * valA;
15
- magB += valB * valB;
68
+ const a = bufferA[i] ? 1 : 0;
69
+ const b = bufferB[i] ? 1 : 0;
70
+ densityA += a;
71
+ densityB += b;
72
+ let iSync = i + offSync;
73
+ if (iSync >= L) iSync -= L;
74
+ let iALeadsB = i + offALeadsB;
75
+ if (iALeadsB >= L) iALeadsB -= L;
76
+ let iBLeadsA = i + offBLeadsA;
77
+ if (iBLeadsA >= L) iBLeadsA -= L;
78
+ if (a && bufferB[iSync]) kSync++;
79
+ if (a && bufferB[iALeadsB]) kALeadsB++;
80
+ if (a && bufferB[iBLeadsA]) kBLeadsA++;
16
81
  }
17
- if (magA === 0 || magB === 0) return 0;
18
- return dot / (Math.sqrt(magA) * Math.sqrt(magB));
82
+ return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
19
83
  };
20
- var calculateCosineSimilarity = (A, B) => {
21
- let dot = 0, magA = 0, magB = 0;
22
- for (let i = 0; i < A.length; i++) {
23
- dot += A[i] * B[i];
24
- magA += A[i] * A[i];
25
- magB += B[i] * B[i];
26
- }
27
- return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
84
+ var cosineFromOverlap = (overlap, densityA, densityB) => {
85
+ if (densityA <= 0 || densityB <= 0) return 0;
86
+ return overlap / Math.sqrt(densityA * densityB);
28
87
  };
29
88
 
30
89
  // src/core/graph.ts
@@ -83,13 +142,6 @@ var groupEventSources = (nodes, edges) => {
83
142
  return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
84
143
  };
85
144
 
86
- // src/core/constants.ts
87
- var WINDOW_SIZE = 50;
88
- var SIMILARITY_THRESHOLD = 0.88;
89
- var LOOP_THRESHOLD = 150;
90
- var VOLATILITY_THRESHOLD = 25;
91
- var INSTANCE_SEP = "##";
92
-
93
145
  // src/core/label.ts
94
146
  var stripInstance = (label) => {
95
147
  const idx = label.indexOf(INSTANCE_SEP);
@@ -203,32 +255,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
203
255
  var LOG_COOLDOWN = 3e3;
204
256
  var THEME = {
205
257
  identity: "#6C5CE7",
206
- // Purple (Brand)
207
258
  problem: "#D63031",
208
- // Red (Bugs)
209
259
  solution: "#FBC531",
210
- // Yellow (Fixes)
211
260
  context: "#0984E3",
212
- // Blue (Locations)
213
261
  muted: "#9AA0A6",
214
- // Gray (Metadata)
215
262
  border: "#2E2E35",
216
263
  success: "#00b894"
217
- // Green (Good Score)
218
264
  };
219
265
  var STYLES = {
220
- // Structure
221
266
  basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
222
267
  headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
223
268
  headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
224
269
  version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
225
- // Actions
226
270
  actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
227
271
  actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
228
- // Context
229
272
  impactLabel: `color: ${THEME.context}; font-weight: bold;`,
230
273
  location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
231
- // Text
232
274
  subText: `color: ${THEME.muted}; font-size: 11px;`,
233
275
  bold: "font-weight: bold;",
234
276
  label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
@@ -243,97 +285,95 @@ var shouldLog = (key) => {
243
285
  return false;
244
286
  };
245
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
+ };
292
+ var areSyncSignificant = (metaA, metaB) => {
293
+ const { kSync, densityA, densityB } = countOverlapsCircular(
294
+ metaA.buffer,
295
+ metaA.head,
296
+ metaB.buffer,
297
+ metaB.head
298
+ );
299
+ return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
300
+ };
246
301
  var getSuggestedFix = (issue, info) => {
247
302
  if (issue.label.includes("Global Event")) {
248
- 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.`;
249
304
  }
250
305
  const violations = issue.violations || [];
251
306
  const leaks = violations.filter((v) => v.type === "causal_leak");
252
307
  const mirrors = violations.filter((v) => v.type === "context_mirror");
253
308
  const duplicates = violations.filter((v) => v.type === "duplicate_state");
254
309
  if (mirrors.length > 0) {
255
- 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.`;
256
311
  }
257
312
  if (leaks.length > 0) {
258
- const targetName = parseLabel(leaks[0].target).name;
313
+ const targetName = displayName(leaks[0].target);
259
314
  if (issue.label.includes("effect")) {
260
- 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.`;
261
316
  }
262
- 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.`;
263
318
  }
264
319
  if (duplicates.length > 0) {
265
320
  if (isBooleanLike(info.name)) {
266
- 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.`;
267
322
  }
268
- 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.`;
269
324
  }
270
325
  if (issue.metric === "density") {
271
- 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.`;
272
327
  }
273
- return `Check the dependency chain of ${info.name}.`;
328
+ return `Inspect ${info.name} and what updates with it.`;
274
329
  };
275
- var displayHealthReport = (history2, threshold, violationMap) => {
330
+ var displayHealthReport = (history2, violationMap) => {
276
331
  if (!isWeb) return;
277
332
  const entries = Array.from(history2.entries());
278
333
  if (entries.length === 0) return;
279
334
  const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
280
- console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerIdentity);
335
+ console.group(`%c BASIS | report `, STYLES.headerIdentity);
281
336
  if (topIssues.length > 0) {
282
- console.log(
283
- `%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
284
- `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
285
- `font-weight: normal; color: ${THEME.muted}; font-style: italic;`
286
- );
337
+ console.log(`%cStart here`, `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`);
287
338
  topIssues.forEach((issue, idx) => {
288
339
  const info = parseLabel(issue.label);
289
- const icon = issue.metric === "influence" ? "\u26A1" : "\u{1F4C8}";
340
+ const icon = issue.metric === "influence" ? "\u2192" : "\u2022";
290
341
  const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
291
- let displayName = info.name;
292
- let displayFile = info.file;
293
- if (issue.label.includes("Global Event")) {
294
- displayName = info.name;
295
- displayFile = info.file;
296
- }
297
342
  console.group(
298
- ` %c${idx + 1}%c ${icon} ${displayName} %c(${displayFile})`,
343
+ ` %c${idx + 1}%c ${icon} ${displayName(issue.label)} %c(${info.file})`,
299
344
  `background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
300
345
  "font-family: monospace; font-weight: 700;",
301
- `color: ${THEME.muted}; font-size: 10px; font-weight: normal; font-style: italic;`
346
+ `color: ${THEME.muted}; font-size: 10px; font-weight: normal;`
302
347
  );
303
- console.log(`%c${issue.reason}`, `color: ${THEME.muted}; font-style: italic;`);
348
+ console.log(`%c${issue.reason}`, `color: ${THEME.muted};`);
304
349
  if (issue.violations.length > 0) {
305
350
  const byFile = /* @__PURE__ */ new Map();
306
351
  issue.violations.forEach((v) => {
307
352
  if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
308
353
  const { file, name } = parseLabel(v.target);
309
354
  if (!byFile.has(file)) byFile.set(file, []);
310
- byFile.get(file).push(name);
355
+ byFile.get(file).push(name.replace(/:\d+$/, ""));
311
356
  });
312
357
  const impactParts = [];
313
358
  byFile.forEach((vars, file) => {
314
- const varList = vars.join(", ");
315
- impactParts.push(`${file} (${varList})`);
359
+ impactParts.push(`${file} (${vars.join(", ")})`);
316
360
  });
317
361
  if (impactParts.length > 0) {
318
- console.log(`%cImpacts: %c${impactParts.join(" + ")}`, STYLES.impactLabel, "");
362
+ console.log(`%cAlso updates: %c${impactParts.join(" \xB7 ")}`, STYLES.impactLabel, "");
319
363
  }
320
364
  }
321
365
  const fix = getSuggestedFix(issue, info);
322
366
  const fixParts = fix.split("%c");
323
367
  if (fixParts.length === 3) {
324
368
  console.log(
325
- `%cSolution: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
369
+ `%cTry: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
326
370
  STYLES.actionLabel,
327
371
  "",
328
372
  STYLES.actionPill,
329
373
  ""
330
374
  );
331
375
  } else {
332
- console.log(
333
- `%cSolution: %c${fix}`,
334
- STYLES.actionLabel,
335
- ""
336
- );
376
+ console.log(`%cTry: %c${fix}`, STYLES.actionLabel, "");
337
377
  }
338
378
  console.groupEnd();
339
379
  });
@@ -348,145 +388,179 @@ var displayHealthReport = (history2, threshold, violationMap) => {
348
388
  processed.add(labelA);
349
389
  entries.forEach(([labelB, metaB]) => {
350
390
  if (labelA === labelB || processed.has(labelB)) return;
351
- if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
352
- if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
353
- currentCluster.push(labelB);
354
- processed.add(labelB);
355
- }
391
+ if (!areSyncSignificant(metaA, metaB)) return;
392
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
393
+ currentCluster.push(labelB);
394
+ processed.add(labelB);
356
395
  });
357
396
  if (currentCluster.length > 1) clusters.push(currentCluster);
358
397
  else independentCount++;
359
398
  });
360
399
  const totalVars = entries.length;
361
- const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
362
- let internalEdges = 0;
363
- instance.graph.forEach((targets, source) => {
364
- if (source.startsWith("Event_Tick_")) return;
365
- internalEdges += targets.size;
366
- });
367
- const causalPenalty = internalEdges / totalVars * 100;
368
- let healthScore = redundancyScore - causalPenalty;
369
- if (healthScore < 0) healthScore = 0;
370
- const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
371
400
  console.log(
372
- `%cSystem Efficiency: %c${healthScore.toFixed(1)}%`,
373
- STYLES.bold,
374
- `color: ${scoreColor}; font-weight: bold;`
401
+ `%c${independentCount + clusters.length} of ${totalVars} instrumented hooks look independent in this window.`,
402
+ STYLES.subText
375
403
  );
376
- console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
377
404
  if (clusters.length > 0) {
378
- 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
+ );
379
409
  clusters.forEach((cluster, idx) => {
380
410
  const clusterMetas = cluster.map((l) => ({
381
411
  label: l,
382
412
  meta: history2.get(l),
383
- name: parseLabel(l).name
413
+ name: displayName(l)
384
414
  }));
385
415
  const hasCtx = clusterMetas.some(
386
416
  (c) => c.meta.role === "context" /* CONTEXT */ || c.meta.role === "store" /* STORE */
387
417
  );
388
- const names = clusterMetas.map((c) => {
389
- const prefix = c.meta.role === "store" /* STORE */ ? "\u03A3 " : c.meta.role === "context" /* CONTEXT */ ? "\u03A9 " : "";
390
- return `${prefix}${c.name}`;
391
- }).join(" \u27F7 ");
392
- 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
+ );
393
424
  if (hasCtx) {
394
425
  const hasStore = clusterMetas.some((c) => c.meta.role === "store" /* STORE */);
395
- const sourceType = hasStore ? "External Store" : "global context";
396
- console.log(`%cDiagnosis: ${hasStore ? "Store" : "Context"} Mirroring. Local state is shadowing ${sourceType}.`, `color: ${THEME.problem};`);
397
- 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
+ );
398
433
  } else {
399
- 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
+ ];
400
447
  const boolCount = clusterMetas.filter(
401
448
  (c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
402
449
  ).length;
403
- const isBoolExplosion = cluster.length > 2 && boolCount / cluster.length > 0.5;
404
- if (isBoolExplosion) {
405
- console.log(`%cDiagnosis:%c Boolean Explosion. Multiple booleans updating in sync.`, STYLES.bold, "");
406
- 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
+ );
407
459
  } else if (cluster.length > 2) {
408
- console.log(`%cDiagnosis:%c Sibling Updates. These states respond to the same event.`, STYLES.bold, "");
409
- 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
+ );
410
468
  } else {
411
- console.log(`%cDiagnosis:%c Redundant State. Variables always change together.`, STYLES.bold, "");
412
- 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
+ );
413
477
  }
414
478
  }
415
479
  console.groupEnd();
416
480
  });
417
481
  } else {
418
- 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
+ );
419
486
  }
420
487
  console.groupEnd();
421
488
  };
422
- var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
489
+ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, overlap) => {
423
490
  if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
424
491
  const infoA = parseLabel(labelA);
425
- const infoB = parseLabel(labelB);
492
+ const nameA = displayName(labelA);
493
+ const nameB = displayName(labelB);
426
494
  const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
427
495
  const isStoreMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "store" /* STORE */ || metaB.role === "local" /* LOCAL */ && metaA.role === "store" /* STORE */;
428
- const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
429
- console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
430
- console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
431
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, 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
+ );
432
507
  if (isContextMirror || isStoreMirror) {
433
- const sourceType = isStoreMirror ? "External Store" : "Global Context";
508
+ const sourceType = isStoreMirror ? "store" : "context";
509
+ console.log(
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)) {
434
517
  console.log(
435
- `%cFix:%c Local state is 'shadowing' ${sourceType}. Delete the local state and consume the %c${sourceType}%c value directly.`,
518
+ `%cTry:%c One %cstatus%c instead of several flags.`,
436
519
  STYLES.bold,
437
520
  "",
438
521
  STYLES.actionPill,
439
522
  ""
440
523
  );
441
524
  } else {
442
- if (isBooleanLike(infoA.name) || isBooleanLike(infoB.name)) {
443
- console.log(
444
- `%cFix:%c Boolean Explosion detected. Merge flags into a single %cstatus%c string or %cuseReducer%c.`,
445
- STYLES.bold,
446
- "",
447
- STYLES.actionPill,
448
- "",
449
- STYLES.actionPill,
450
- ""
451
- );
452
- } else {
453
- console.log(
454
- `%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
455
- STYLES.bold,
456
- "",
457
- STYLES.label,
458
- "",
459
- STYLES.label,
460
- "",
461
- STYLES.actionPill,
462
- ""
463
- );
464
- }
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
+ );
465
534
  }
466
535
  console.groupEnd();
467
536
  };
468
- var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
537
+ var displayCausalHint = (targetLabel, _targetMeta, sourceLabel, sourceMeta) => {
469
538
  if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
470
539
  const target = parseLabel(targetLabel);
471
- const source = parseLabel(sourceLabel);
472
- 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";
473
543
  const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
474
- console.groupCollapsed(`%c \u26A1 BASIS | ${headerType} `, STYLES.headerProblem);
475
- console.log(`%c\u{1F4CD} Location: %c${target.file}`, STYLES.bold, STYLES.location);
476
- 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
+ );
477
553
  if (isEffect) {
478
554
  console.log(
479
- `%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.`,
480
556
  STYLES.bold,
481
557
  "",
482
558
  STYLES.label,
483
- "",
484
- STYLES.actionPill,
485
559
  ""
486
560
  );
487
561
  } else {
488
562
  console.log(
489
- `%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.`,
490
564
  STYLES.bold,
491
565
  "",
492
566
  STYLES.label,
@@ -519,7 +593,7 @@ var displayGraphReport = (graph) => {
519
593
  if (!isWeb) return;
520
594
  if (graph.nodes.length === 0) {
521
595
  console.log(
522
- `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
596
+ `%c BASIS | update graph %c(nothing recorded yet)`,
523
597
  STYLES.headerIdentity,
524
598
  `color: ${THEME.muted}; font-style: italic;`
525
599
  );
@@ -538,17 +612,22 @@ var displayGraphReport = (graph) => {
538
612
  occurrences: g.occurrences
539
613
  }));
540
614
  const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
541
- 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
+ }));
542
621
  const groups = [...eventGroups, ...nonEventGroups].sort(
543
622
  (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
544
623
  );
545
624
  console.group(
546
- `%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`,
547
626
  STYLES.headerIdentity,
548
- `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
627
+ `color: ${THEME.muted}; font-weight: normal;`
549
628
  );
550
629
  console.log(
551
- `%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.`,
552
631
  STYLES.subText
553
632
  );
554
633
  groups.forEach((group) => {
@@ -556,12 +635,12 @@ var displayGraphReport = (graph) => {
556
635
  const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
557
636
  const isFx = group.sourceNode?.role === "effect";
558
637
  const isUnknown = group.sourceNode?.role === "unknown";
559
- const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
638
+ const icon = isEvent ? "\u2022" : isCtx ? "ctx" : isFx ? "fx" : isUnknown ? "?" : "\u2022";
560
639
  const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
561
640
  const fanout = group.edges.length;
562
641
  const hits = group.occurrences;
563
642
  const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
564
- 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]);
565
644
  console.groupCollapsed(
566
645
  `%c${icon} %c${title}`,
567
646
  `color: ${color};`,
@@ -573,16 +652,16 @@ var displayGraphReport = (graph) => {
573
652
  const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
574
653
  if (target?.redundant) {
575
654
  console.log(
576
- `%c ${label}%c${weight} %credundant`,
655
+ `%c ${label}%c${weight} %cmoving with another hook`,
577
656
  `color: ${THEME.muted}; font-family: monospace;`,
578
- `color: ${THEME.muted}; font-style: italic;`,
657
+ `color: ${THEME.muted};`,
579
658
  `color: ${THEME.problem}; font-weight: bold;`
580
659
  );
581
660
  } else {
582
661
  console.log(
583
662
  `%c ${label}%c${weight}`,
584
663
  `color: ${THEME.muted}; font-family: monospace;`,
585
- `color: ${THEME.muted}; font-style: italic;`
664
+ `color: ${THEME.muted};`
586
665
  );
587
666
  }
588
667
  });
@@ -590,23 +669,29 @@ var displayGraphReport = (graph) => {
590
669
  });
591
670
  console.groupEnd();
592
671
  };
593
- var displayViolentBreaker = (label, count, threshold) => {
672
+ var displayViolentBreaker = (label, count, _threshold) => {
594
673
  if (!isWeb) return;
595
- const { name } = parseLabel(label);
596
- console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, STYLES.headerProblem);
597
- console.error(`INFINITE LOOP DETECTED
598
- Variable: ${name}
599
- Frequency: ${count} updates/sec`);
600
- 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
+ );
601
683
  console.groupEnd();
602
684
  };
603
685
  var displayBootLog = (windowSize) => {
604
686
  if (!isWeb) return;
605
- 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
+ );
606
692
  };
607
693
 
608
694
  // src/core/analysis.ts
609
- var CAUSAL_MARGIN = 0.05;
610
695
  var isEventDriven = (label, graph) => {
611
696
  for (const [parent, targets] of graph.entries()) {
612
697
  if (parent.startsWith("Event_Tick_") && targets.has(label)) {
@@ -616,29 +701,40 @@ var isEventDriven = (label, graph) => {
616
701
  return false;
617
702
  };
618
703
  var calculateAllSimilarities = (entryA, entryB) => {
619
- const sync = calculateSimilarityCircular(
620
- entryA.meta.buffer,
621
- entryA.meta.head,
622
- entryB.meta.buffer,
623
- entryB.meta.head,
624
- 0
625
- );
626
- const bA = calculateSimilarityCircular(
704
+ const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
627
705
  entryA.meta.buffer,
628
706
  entryA.meta.head,
629
707
  entryB.meta.buffer,
630
- entryB.meta.head,
631
- 1
708
+ entryB.meta.head
632
709
  );
633
- const aB = calculateSimilarityCircular(
634
- entryA.meta.buffer,
635
- entryA.meta.head,
636
- entryB.meta.buffer,
637
- entryB.meta.head,
638
- -1
639
- );
640
- return { sync, bA, aB, max: Math.max(sync, bA, aB) };
710
+ const sync = cosineFromOverlap(kSync, densityA, densityB);
711
+ const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
712
+ const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
713
+ const max = Math.max(sync, bA, aB);
714
+ const windowSize = entryA.meta.buffer.length;
715
+ const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
716
+ const kLead = Math.max(kALeadsB, kBLeadsA);
717
+ const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
718
+ return {
719
+ sync,
720
+ bA,
721
+ aB,
722
+ max,
723
+ kSync,
724
+ kALeadsB,
725
+ kBLeadsA,
726
+ densityA,
727
+ densityB,
728
+ significantSync,
729
+ significantLead
730
+ };
641
731
  };
732
+ var overlapFrom = (s) => ({
733
+ kSync: s.kSync,
734
+ densityA: s.densityA,
735
+ densityB: s.densityB,
736
+ cosine: s.sync
737
+ });
642
738
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
643
739
  if (entryA.label === entryB.label) return true;
644
740
  if (isSameField(entryA.label, entryB.label)) return true;
@@ -662,52 +758,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
662
758
  const roleA = entryA.meta.role;
663
759
  const roleB = entryB.meta.role;
664
760
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
665
- if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
761
+ if (similarities.densityA < 2 || similarities.densityB < 2) return;
762
+ const overlap = overlapFrom(similarities);
666
763
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
667
764
  redundantSet.add(entryA.label);
668
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
669
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
765
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, overlap });
766
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, overlap);
670
767
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
671
768
  redundantSet.add(entryB.label);
672
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
673
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
769
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, overlap });
770
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, overlap);
674
771
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
675
772
  redundantSet.add(entryA.label);
676
773
  redundantSet.add(entryB.label);
677
- pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
678
- pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
679
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
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);
680
777
  }
681
778
  };
682
779
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
683
780
  if (entryA.isVolatile || entryB.isVolatile) return;
684
- if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
685
781
  const addLeak = (source, target) => {
686
782
  if (isEventDriven(target, graph)) return;
687
- if (!violationMap.has(source)) {
688
- violationMap.set(source, []);
689
- }
690
- violationMap.get(source).push({ type: "causal_leak", target });
783
+ pushViolation(violationMap, source, { type: "causal_leak", target });
691
784
  const sourceEntry = source === entryA.label ? entryA : entryB;
692
785
  const targetEntry = source === entryA.label ? entryB : entryA;
693
786
  displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
694
787
  };
695
- if (similarities.bA === similarities.max) {
788
+ if (similarities.kALeadsB >= similarities.kBLeadsA) {
696
789
  addLeak(entryA.label, entryB.label);
697
- } else if (similarities.aB === similarities.max) {
790
+ } else {
698
791
  addLeak(entryB.label, entryA.label);
699
792
  }
700
793
  };
701
794
  var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
702
- let compCount = 0;
703
795
  const violationMap = /* @__PURE__ */ new Map();
796
+ let compCount = 0;
704
797
  for (const entryA of dirtyEntries) {
705
798
  for (const entryB of allEntries) {
706
799
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
707
800
  compCount++;
708
801
  const similarities = calculateAllSimilarities(entryA, entryB);
709
- if (similarities.max > SIMILARITY_THRESHOLD) {
802
+ if (similarities.significantSync) {
710
803
  detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
804
+ }
805
+ if (similarities.significantLead) {
711
806
  detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
712
807
  }
713
808
  }
@@ -966,9 +1061,9 @@ var beginEffectTracking = (l) => {
966
1061
  var endEffectTracking = () => {
967
1062
  instance.currentEffectSource = null;
968
1063
  };
969
- var printBasisHealthReport = (threshold = 0.5) => {
1064
+ var printBasisHealthReport = () => {
970
1065
  if (!instance.config.debug) return;
971
- displayHealthReport(instance.history, threshold, instance.violationMap);
1066
+ displayHealthReport(instance.history, instance.violationMap);
972
1067
  };
973
1068
  var getBasisMetrics = () => ({
974
1069
  engine: "v0.6.x",
@@ -1042,4 +1137,4 @@ export {
1042
1137
  getBasisGraph,
1043
1138
  printBasisGraph
1044
1139
  };
1045
- //# sourceMappingURL=chunk-EJXGN76H.mjs.map
1140
+ //# sourceMappingURL=chunk-KIXM6YRX.mjs.map