react-state-basis 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -91,24 +91,176 @@ var calculateCosineSimilarity = (A, B) => {
91
91
  return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
92
92
  };
93
93
 
94
+ // src/core/graph.ts
95
+ var calculateSpectralInfluence = (graph, maxIterations = 20, tolerance = 1e-3) => {
96
+ const nodes = Array.from(/* @__PURE__ */ new Set([...graph.keys(), ...Array.from(graph.values()).flatMap((m) => [...m.keys()])]));
97
+ if (nodes.length === 0) return /* @__PURE__ */ new Map();
98
+ let scores = /* @__PURE__ */ new Map();
99
+ nodes.forEach((n) => scores.set(n, 1 / nodes.length));
100
+ for (let i = 0; i < maxIterations; i++) {
101
+ const nextScores = /* @__PURE__ */ new Map();
102
+ let totalWeight = 0;
103
+ nodes.forEach((source) => {
104
+ let influence = 0;
105
+ const outgoing = graph.get(source);
106
+ if (outgoing) {
107
+ outgoing.forEach((weight, target) => {
108
+ if (source !== target) {
109
+ influence += (scores.get(target) || 0) * weight;
110
+ }
111
+ });
112
+ }
113
+ nextScores.set(source, influence + 0.01);
114
+ totalWeight += influence + 0.01;
115
+ });
116
+ let delta = 0;
117
+ nextScores.forEach((val, key) => {
118
+ const normalized = val / totalWeight;
119
+ const diff = normalized - (scores.get(key) || 0);
120
+ delta += diff * diff;
121
+ nextScores.set(key, normalized);
122
+ });
123
+ scores = nextScores;
124
+ if (Math.sqrt(delta) < tolerance) break;
125
+ }
126
+ return scores;
127
+ };
128
+
129
+ // src/core/ranker.ts
130
+ var parseLabel = (label) => {
131
+ const parts = label.split(" -> ");
132
+ return { file: parts[0] || "Unknown", name: parts[1] || label };
133
+ };
134
+ var identifyTopIssues = (graph, history2, redundantLabels2, violationMap) => {
135
+ const results = [];
136
+ const influence = calculateSpectralInfluence(graph);
137
+ const isEffect = (label) => label.includes("effect_L") || label.includes("useLayoutEffect") || label.includes("useInsertionEffect");
138
+ const isEvent = (label) => label.startsWith("Event_Tick_");
139
+ const eventSignatures = /* @__PURE__ */ new Map();
140
+ const drivers = Array.from(graph.entries()).filter(([label, targets]) => {
141
+ if (targets.size === 0) return false;
142
+ if (isEvent(label)) {
143
+ const validTargets = Array.from(targets.keys()).filter((t) => {
144
+ const meta2 = history2.get(t);
145
+ return meta2 && meta2.role !== "context" /* CONTEXT */;
146
+ });
147
+ if (validTargets.length > 1) {
148
+ const signature = validTargets.sort().join("|");
149
+ const existing = eventSignatures.get(signature);
150
+ if (existing) {
151
+ existing.count++;
152
+ existing.score = Math.max(existing.score, influence.get(label) || 0);
153
+ } else {
154
+ eventSignatures.set(signature, {
155
+ count: 1,
156
+ score: influence.get(label) || 0,
157
+ targets: validTargets
158
+ });
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+ const meta = history2.get(label);
164
+ if (meta?.role === "context" /* CONTEXT */) return false;
165
+ if (meta?.role === "proj" /* PROJECTION */) return false;
166
+ const score = influence.get(label) || 0;
167
+ if (isEffect(label) && targets.size > 0) return true;
168
+ if (targets.size < 2 && score < 0.05) return false;
169
+ return true;
170
+ }).sort((a, b) => {
171
+ const scoreA = influence.get(a[0]) || 0;
172
+ const scoreB = influence.get(b[0]) || 0;
173
+ return scoreB - scoreA;
174
+ });
175
+ const sortedEvents = Array.from(eventSignatures.entries()).sort((a, b) => b[1].targets.length - a[1].targets.length);
176
+ sortedEvents.slice(0, 2).forEach(([sig, data]) => {
177
+ const primaryVictim = parseLabel(data.targets[0]);
178
+ const smartLabel = `${primaryVictim.file} -> Global Event (${primaryVictim.name})`;
179
+ results.push({
180
+ label: smartLabel,
181
+ metric: "influence",
182
+ score: 1,
183
+ reason: `Global Sync Event: An external trigger is updating ${data.targets.length} roots simultaneously. Occurred ${data.count} times.`,
184
+ violations: data.targets.map((t) => ({
185
+ type: "causal_leak",
186
+ target: t
187
+ }))
188
+ });
189
+ });
190
+ drivers.slice(0, 3 - results.length).forEach(([label, targets]) => {
191
+ const targetNames = Array.from(targets.keys());
192
+ if (isEffect(label)) {
193
+ results.push({
194
+ label,
195
+ metric: "influence",
196
+ score: influence.get(label) || 0,
197
+ reason: `Side-Effect Driver: Hook writes to state during render.`,
198
+ violations: targetNames.map((t) => ({ type: "causal_leak", target: t }))
199
+ });
200
+ return;
201
+ }
202
+ results.push({
203
+ label,
204
+ metric: "influence",
205
+ score: influence.get(label) || targets.size,
206
+ reason: `Sync Driver: Acts as a "Prime Mover" for ${targets.size} downstream signals.`,
207
+ violations: targetNames.map((t) => ({ type: "causal_leak", target: t }))
208
+ });
209
+ });
210
+ if (results.length === 0) {
211
+ const sortedDensity = Array.from(history2.entries()).filter(
212
+ ([label, meta]) => meta.role === "local" /* LOCAL */ && !redundantLabels2.has(label) && meta.density > 25
213
+ ).sort((a, b) => b[1].density - a[1].density);
214
+ sortedDensity.slice(0, 3).forEach(([label, meta]) => {
215
+ results.push({
216
+ label,
217
+ metric: "density",
218
+ score: meta.density,
219
+ reason: `High Frequency: potential main-thread saturation.`,
220
+ violations: []
221
+ });
222
+ });
223
+ }
224
+ return results;
225
+ };
226
+
94
227
  // src/core/logger.ts
95
228
  var isWeb = typeof window !== "undefined" && typeof window.document !== "undefined";
96
229
  var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
97
230
  var LOG_COOLDOWN = 3e3;
231
+ var THEME = {
232
+ identity: "#6C5CE7",
233
+ // Purple (Brand)
234
+ problem: "#D63031",
235
+ // Red (Bugs)
236
+ solution: "#FBC531",
237
+ // Yellow (Fixes)
238
+ context: "#0984E3",
239
+ // Blue (Locations)
240
+ muted: "#9AA0A6",
241
+ // Gray (Metadata)
242
+ border: "#2E2E35",
243
+ success: "#00b894"
244
+ // Green (Good Score)
245
+ };
98
246
  var STYLES = {
99
- basis: "background: #6c5ce7; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;",
100
- version: "background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;",
101
- headerRed: "background: #d63031; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;",
102
- headerBlue: "background: #0984e3; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;",
103
- headerGreen: "background: #00b894; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;",
104
- label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;",
105
- location: "color: #0984e3; font-family: monospace; font-weight: bold;",
106
- subText: "color: #636e72; font-size: 11px;",
247
+ // Structure
248
+ basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
249
+ headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
250
+ headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
251
+ version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
252
+ // Actions
253
+ actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
254
+ actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
255
+ // Context
256
+ impactLabel: `color: ${THEME.context}; font-weight: bold;`,
257
+ location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
258
+ // Text
259
+ subText: `color: ${THEME.muted}; font-size: 11px;`,
107
260
  bold: "font-weight: bold;",
108
- action: "color: #00b894; font-weight: bold; border: 1px solid #00b894; padding: 0 4px; border-radius: 3px;",
109
- warning: "color: #d63031; font-weight: bold; border: 1px solid #d63031; padding: 0 4px; border-radius: 3px;"
261
+ label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
110
262
  };
111
- var parseLabel = (label) => {
263
+ var parseLabel2 = (label) => {
112
264
  const parts = label.split(" -> ");
113
265
  return { file: parts[0] || "Unknown", name: parts[1] || label };
114
266
  };
@@ -121,57 +273,103 @@ var shouldLog = (key) => {
121
273
  }
122
274
  return false;
123
275
  };
124
- var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
125
- if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
126
- const infoA = parseLabel(labelA);
127
- const infoB = parseLabel(labelB);
128
- const isContextMirror = metaA.role === "local" && metaB.role === "context" || metaB.role === "local" && metaA.role === "context";
129
- const local = metaA.role === "local" ? infoA : infoB;
130
- const context = metaA.role === "context" ? infoA : infoB;
131
- console.group(`%c \u264A BASIS | ${isContextMirror ? "CONTEXT MIRRORING" : "DUPLICATE STATE"} `, STYLES.headerRed);
132
- console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
133
- if (isContextMirror) {
134
- console.log(
135
- `%cIssue:%c Local variable %c${local.name}%c is just a copy of Global Context %c${context.name}%c.
136
- %cConfidence: ${(sim * 100).toFixed(0)}%`,
137
- STYLES.bold,
138
- "",
139
- STYLES.label,
140
- "",
141
- STYLES.label,
142
- "",
143
- STYLES.subText
144
- );
145
- console.log(`%cFix:%c Use the context value directly to avoid state drift.`, STYLES.bold, STYLES.warning);
146
- } else {
147
- console.log(
148
- `%cIssue:%c %c${infoA.name}%c and %c${infoB.name}%c are synchronized (${(sim * 100).toFixed(0)}% correlation).`,
149
- STYLES.bold,
150
- "",
151
- STYLES.label,
152
- "",
153
- STYLES.label,
154
- ""
155
- );
156
- console.log(
157
- `%cFix:%c Merge states or calculate %c${infoB.name}%c from %c${infoA.name}%c via %cuseMemo%c.`,
158
- STYLES.bold,
159
- "",
160
- STYLES.label,
161
- "",
162
- STYLES.label,
163
- "",
164
- STYLES.action,
165
- ""
166
- );
276
+ var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
277
+ var getSuggestedFix = (issue, info) => {
278
+ if (issue.label.includes("Global Event")) {
279
+ return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
167
280
  }
168
- console.groupEnd();
281
+ const violations = issue.violations || [];
282
+ const leaks = violations.filter((v) => v.type === "causal_leak");
283
+ const mirrors = violations.filter((v) => v.type === "context_mirror");
284
+ const duplicates = violations.filter((v) => v.type === "duplicate_state");
285
+ if (mirrors.length > 0) {
286
+ return `Local state is 'shadowing' Global Context. This creates two sources of truth. Delete the local state and consume the %cContext%c value directly.`;
287
+ }
288
+ if (leaks.length > 0) {
289
+ const targetName = parseLabel2(leaks[0].target).name;
290
+ if (issue.label.includes("effect")) {
291
+ 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.`;
292
+ }
293
+ return `State cascading detected. ${info.name} triggers ${targetName} in a separate frame. Merge them into one object to update simultaneously.`;
294
+ }
295
+ if (duplicates.length > 0) {
296
+ if (isBooleanLike(info.name)) {
297
+ return `Boolean Explosion detected. Multiple flags are toggling in sync. Replace impossible states with a single %cstatus%c string ('idle' | 'loading' | 'success').`;
298
+ }
299
+ 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.`;
300
+ }
301
+ if (issue.metric === "density") {
302
+ 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.`;
303
+ }
304
+ return `Check the dependency chain of ${info.name}.`;
169
305
  };
170
- var displayHealthReport = (history2, threshold) => {
306
+ var displayHealthReport = (history2, threshold, violationMap) => {
171
307
  if (!isWeb) return;
172
308
  const entries = Array.from(history2.entries());
173
- const totalVars = entries.length;
174
- if (totalVars === 0) return;
309
+ if (entries.length === 0) return;
310
+ const topIssues = identifyTopIssues(instance.graph, history2, instance.redundantLabels, violationMap);
311
+ console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerIdentity);
312
+ if (topIssues.length > 0) {
313
+ console.log(
314
+ `%c\u{1F3AF} REFACTOR PRIORITIES %c(PRIME MOVERS)`,
315
+ `font-weight: bold; color: ${THEME.identity}; margin-top: 10px;`,
316
+ `font-weight: normal; color: ${THEME.muted}; font-style: italic;`
317
+ );
318
+ topIssues.forEach((issue, idx) => {
319
+ const info = parseLabel2(issue.label);
320
+ const icon = issue.metric === "influence" ? "\u26A1" : "\u{1F4C8}";
321
+ const pColor = idx === 0 ? THEME.problem : idx === 1 ? THEME.solution : THEME.identity;
322
+ let displayName = info.name;
323
+ let displayFile = info.file;
324
+ if (issue.label.includes("Global Event")) {
325
+ displayName = info.name;
326
+ displayFile = info.file;
327
+ }
328
+ console.group(
329
+ ` %c${idx + 1}%c ${icon} ${displayName} %c(${displayFile})`,
330
+ `background: ${pColor}; color: ${idx === 1 ? "black" : "white"}; border-radius: 50%; padding: 0 5px;`,
331
+ "font-family: monospace; font-weight: 700;",
332
+ `color: ${THEME.muted}; font-size: 10px; font-weight: normal; font-style: italic;`
333
+ );
334
+ console.log(`%c${issue.reason}`, `color: ${THEME.muted}; font-style: italic;`);
335
+ if (issue.violations.length > 0) {
336
+ const byFile = /* @__PURE__ */ new Map();
337
+ issue.violations.forEach((v) => {
338
+ if (issue.label.includes("Global Event") && v.type === "context_mirror") return;
339
+ const { file, name } = parseLabel2(v.target);
340
+ if (!byFile.has(file)) byFile.set(file, []);
341
+ byFile.get(file).push(name);
342
+ });
343
+ const impactParts = [];
344
+ byFile.forEach((vars, file) => {
345
+ const varList = vars.join(", ");
346
+ impactParts.push(`${file} (${varList})`);
347
+ });
348
+ if (impactParts.length > 0) {
349
+ console.log(`%cImpacts: %c${impactParts.join(" + ")}`, STYLES.impactLabel, "");
350
+ }
351
+ }
352
+ const fix = getSuggestedFix(issue, info);
353
+ const fixParts = fix.split("%c");
354
+ if (fixParts.length === 3) {
355
+ console.log(
356
+ `%cSolution: %c${fixParts[0]}%c${fixParts[1]}%c${fixParts[2]}`,
357
+ STYLES.actionLabel,
358
+ "",
359
+ STYLES.actionPill,
360
+ ""
361
+ );
362
+ } else {
363
+ console.log(
364
+ `%cSolution: %c${fix}`,
365
+ STYLES.actionLabel,
366
+ ""
367
+ );
368
+ }
369
+ console.groupEnd();
370
+ });
371
+ console.log("\n");
372
+ }
175
373
  const clusters = [];
176
374
  const processed = /* @__PURE__ */ new Set();
177
375
  let independentCount = 0;
@@ -181,9 +379,8 @@ var displayHealthReport = (history2, threshold) => {
181
379
  processed.add(labelA);
182
380
  entries.forEach(([labelB, metaB]) => {
183
381
  if (labelA === labelB || processed.has(labelB)) return;
184
- const sim = calculateCosineSimilarity(metaA.buffer, metaB.buffer);
185
- if (sim > threshold) {
186
- if (metaA.role === "context" && metaB.role === "context") return;
382
+ if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
383
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
187
384
  currentCluster.push(labelB);
188
385
  processed.add(labelB);
189
386
  }
@@ -191,73 +388,149 @@ var displayHealthReport = (history2, threshold) => {
191
388
  if (currentCluster.length > 1) clusters.push(currentCluster);
192
389
  else independentCount++;
193
390
  });
194
- const systemRank = independentCount + clusters.length;
195
- const healthScore = systemRank / totalVars * 100;
196
- console.group(`%c \u{1F4CA} BASIS | ARCHITECTURAL HEALTH REPORT `, STYLES.headerGreen);
391
+ const totalVars = entries.length;
392
+ const redundancyScore = (independentCount + clusters.length) / totalVars * 100;
393
+ let internalEdges = 0;
394
+ instance.graph.forEach((targets, source) => {
395
+ if (source.startsWith("Event_Tick_")) return;
396
+ internalEdges += targets.size;
397
+ });
398
+ const causalPenalty = internalEdges / totalVars * 100;
399
+ let healthScore = redundancyScore - causalPenalty;
400
+ if (healthScore < 0) healthScore = 0;
401
+ const scoreColor = healthScore > 85 ? THEME.success : THEME.problem;
197
402
  console.log(
198
- `%cEfficiency: %c${healthScore.toFixed(1)}% %c(${systemRank}/${totalVars} Sources of Truth)`,
403
+ `%cSystem Efficiency: %c${healthScore.toFixed(1)}%`,
199
404
  STYLES.bold,
200
- `color: ${healthScore > 85 ? "#00b894" : "#d63031"}; font-weight: bold;`,
201
- STYLES.subText
405
+ `color: ${scoreColor}; font-weight: bold;`
202
406
  );
407
+ console.log(`%cSources of Truth: ${independentCount + clusters.length}/${totalVars} | Causal Leaks: ${internalEdges}`, STYLES.subText);
203
408
  if (clusters.length > 0) {
204
- console.log(`%cDetected ${clusters.length} Sync Issues:`, "font-weight: bold; color: #e17055; margin-top: 10px;");
409
+ console.log(`%cDetected ${clusters.length} Sync Issues:`, `font-weight: bold; color: ${THEME.problem}; margin-top: 10px;`);
205
410
  clusters.forEach((cluster, idx) => {
206
- const clusterMetas = cluster.map((l) => ({ label: l, meta: history2.get(l), info: parseLabel(l) }));
207
- const contexts = clusterMetas.filter((c) => c.meta.role === "context");
208
- const locals = clusterMetas.filter((c) => c.meta.role === "local");
209
- const names = clusterMetas.map((c) => `${c.meta.role === "context" ? "\u03A9 " : ""}${c.info.name}`).join(" \u27F7 ");
210
- console.group(` %c${idx + 1}%c ${names}`, "background: #e17055; color: white; border-radius: 50%; padding: 0 5px;", "font-family: monospace; font-weight: bold;");
211
- if (contexts.length > 0) {
212
- const ctxNames = contexts.map((c) => c.info.name).join(", ");
213
- console.log(`%cDiagnosis:%c Context Mirroring. Variables are copying from %c${ctxNames}%c.`, STYLES.bold, "", STYLES.label, "");
214
- console.log(`%cSolution:%c Use the context directly to avoid state drift.`, STYLES.bold, STYLES.action);
411
+ const clusterMetas = cluster.map((l) => ({
412
+ label: l,
413
+ meta: history2.get(l),
414
+ name: parseLabel2(l).name
415
+ }));
416
+ const hasCtx = clusterMetas.some((c) => c.meta.role === "context" /* CONTEXT */);
417
+ const names = clusterMetas.map((c) => `${c.meta.role === "context" /* CONTEXT */ ? "\u03A9 " : ""}${c.name}`).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;");
419
+ if (hasCtx) {
420
+ console.log(`%cDiagnosis: Context Mirroring. Local state is shadowing global context.`, `color: ${THEME.problem};`);
421
+ console.log(`%cSolution: Use context directly to avoid state drift.`, STYLES.actionLabel);
215
422
  } else {
216
- const isExplosion = locals.length > 2;
217
- if (isExplosion) {
218
- console.log(`%cDiagnosis:%c Boolean Explosion. Multiple states updating in sync.`, STYLES.bold, "");
219
- console.log(`%cSolution:%c Combine into a single %cstatus%c string or a %creducer%c.`, STYLES.bold, "", STYLES.label, "", STYLES.label, "");
423
+ const boolKeywords = ["is", "has", "can", "should", "loading", "success", "error", "active", "enabled", "open", "visible"];
424
+ const boolCount = clusterMetas.filter(
425
+ (c) => boolKeywords.some((kw) => c.name.toLowerCase().startsWith(kw))
426
+ ).length;
427
+ const isBoolExplosion = cluster.length > 2 && boolCount / cluster.length > 0.5;
428
+ if (isBoolExplosion) {
429
+ console.log(`%cDiagnosis:%c Boolean Explosion. Multiple booleans updating in sync.`, STYLES.bold, "");
430
+ console.log(`%cSolution:%c Combine into a single %cstatus%c string or a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "", STYLES.actionPill, "");
431
+ } else if (cluster.length > 2) {
432
+ console.log(`%cDiagnosis:%c Sibling Updates. These states respond to the same event.`, STYLES.bold, "");
433
+ console.log(`%cSolution:%c This may be intentional. If not, consolidate into a %creducer%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
220
434
  } else {
221
435
  console.log(`%cDiagnosis:%c Redundant State. Variables always change together.`, STYLES.bold, "");
222
- console.log(`%cSolution:%c Derive one from the other via %cuseMemo%c.`, STYLES.bold, "", STYLES.label, "");
436
+ console.log(`%cSolution:%c Derive one from the other via %cuseMemo%c.`, STYLES.actionLabel, "", STYLES.actionPill, "");
223
437
  }
224
438
  }
225
439
  console.groupEnd();
226
440
  });
227
441
  } else {
228
- console.log("%c\u2728 Your architecture is clean. No redundant state detected.", "color: #00b894; font-weight: bold;");
442
+ console.log("%c\u2728 Your architecture is clean. No redundant state detected.", `color: ${THEME.success}; font-weight: bold;`);
443
+ }
444
+ console.groupEnd();
445
+ };
446
+ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
447
+ if (!isWeb || !shouldLog(`redundant-${labelA}-${labelB}`)) return;
448
+ const infoA = parseLabel2(labelA);
449
+ const infoB = parseLabel2(labelB);
450
+ const isContextMirror = metaA.role === "local" /* LOCAL */ && metaB.role === "context" /* CONTEXT */ || metaB.role === "local" /* LOCAL */ && metaA.role === "context" /* CONTEXT */;
451
+ console.group(`%c \u264A BASIS | ${isContextMirror ? "CONTEXT MIRRORING" : "DUPLICATE STATE"} `, STYLES.headerProblem);
452
+ console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
453
+ console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, STYLES.bold, "");
454
+ if (isContextMirror) {
455
+ console.log(
456
+ `%cFix:%c Local state is 'shadowing' Global Context. Delete the local state and consume the %cContext%c value directly.`,
457
+ STYLES.bold,
458
+ "",
459
+ STYLES.actionPill,
460
+ ""
461
+ );
462
+ } else {
463
+ if (isBooleanLike(infoA.name) || isBooleanLike(infoB.name)) {
464
+ console.log(
465
+ `%cFix:%c Boolean Explosion detected. Merge flags into a single %cstatus%c string or %cuseReducer%c.`,
466
+ STYLES.bold,
467
+ "",
468
+ STYLES.actionPill,
469
+ "",
470
+ STYLES.actionPill,
471
+ ""
472
+ );
473
+ } else {
474
+ console.log(
475
+ `%cFix:%c Redundant State detected. Derive %c${infoB.name}%c from %c${infoA.name}%c during render, or use %cuseMemo%c.`,
476
+ STYLES.bold,
477
+ "",
478
+ STYLES.label,
479
+ "",
480
+ STYLES.label,
481
+ "",
482
+ STYLES.actionPill,
483
+ ""
484
+ );
485
+ }
229
486
  }
230
487
  console.groupEnd();
231
488
  };
232
489
  var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
233
490
  if (!isWeb || !shouldLog(`causal-${sourceLabel}-${targetLabel}`)) return;
234
- const target = parseLabel(targetLabel);
235
- const source = parseLabel(sourceLabel);
236
- const isContextTrigger = sourceMeta.role === "context";
237
- console.groupCollapsed(`%c \u26A1 BASIS | ${isContextTrigger ? "CONTEXT SYNC LEAK" : "DOUBLE RENDER"} `, STYLES.headerBlue);
491
+ const target = parseLabel2(targetLabel);
492
+ const source = parseLabel2(sourceLabel);
493
+ const isCtx = sourceMeta.role === "context" /* CONTEXT */;
494
+ const isEffect = sourceLabel.includes("effect") || sourceLabel.includes("useLayoutEffect");
495
+ console.groupCollapsed(`%c \u26A1 BASIS | ${isCtx ? "CONTEXT SYNC LEAK" : "DOUBLE RENDER"} `, STYLES.headerProblem);
238
496
  console.log(`%c\u{1F4CD} Location: %c${target.file}`, STYLES.bold, STYLES.location);
239
- if (isContextTrigger) {
240
- console.log(`%cIssue:%c Context %c${source.name}%c updated, then local %c${target.name}%c followed 1 frame later.`, STYLES.bold, "", STYLES.label, "", STYLES.label, "");
241
- console.log(`%cImpact: This forces React to render the component twice for every change.`, STYLES.subText);
497
+ console.log(`%cIssue:%c ${source.name} triggers ${target.name} in separate frames.`, STYLES.bold, "");
498
+ if (isEffect) {
499
+ console.log(
500
+ `%cFix:%c Derive %c${target.name}%c during the render phase (remove effect) or wrap in %cuseMemo%c.`,
501
+ STYLES.bold,
502
+ "",
503
+ STYLES.label,
504
+ "",
505
+ STYLES.actionPill,
506
+ ""
507
+ );
242
508
  } else {
243
- console.log(`%cIssue:%c %c${source.name}%c triggers %c${target.name}%c in a separate frame.`, STYLES.bold, "", STYLES.label, "", STYLES.label, "");
509
+ console.log(
510
+ `%cFix:%c Merge %c${target.name}%c with %c${source.name}%c into a single state update.`,
511
+ STYLES.bold,
512
+ "",
513
+ STYLES.label,
514
+ "",
515
+ STYLES.label,
516
+ ""
517
+ );
244
518
  }
245
- console.log(`%cFix:%c Derive %c${target.name}%c during the first render.`, STYLES.bold, STYLES.action, STYLES.label, "");
246
519
  console.groupEnd();
247
520
  };
248
521
  var displayViolentBreaker = (label, count, threshold) => {
249
522
  if (!isWeb) return;
250
523
  const parts = label.split(" -> ");
251
- console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, "background: #dc2626; color: white; font-weight: bold; padding: 8px 16px;");
524
+ console.group(`%c \u{1F6D1} BASIS CRITICAL | CIRCUIT BREAKER `, STYLES.headerProblem);
252
525
  console.error(`INFINITE LOOP DETECTED
253
526
  Variable: ${parts[1] || label}
254
527
  Frequency: ${count} updates/sec`);
255
- console.log(`%cACTION: Update BLOCKED to prevent browser freeze.`, "color: #dc2626; font-weight: bold;");
528
+ console.log(`%cACTION: Update BLOCKED to prevent browser freeze.`, `color: ${THEME.problem}; font-weight: bold;`);
256
529
  console.groupEnd();
257
530
  };
258
531
  var displayBootLog = (windowSize) => {
259
532
  if (!isWeb) return;
260
- console.log(`%cBasis%cAuditor%c | v0.5.x Architectural Forensics Active (Window: ${windowSize})`, STYLES.basis, STYLES.version, "color: #636e72; font-style: italic; margin-left: 8px;");
533
+ console.log(`%cBasis%cAuditor%c "Graph Era" (Window: ${windowSize})`, STYLES.basis, STYLES.version, `color: ${THEME.muted}; font-style: italic; margin-left: 8px;`);
261
534
  };
262
535
 
263
536
  // src/core/constants.ts
@@ -267,10 +540,37 @@ var LOOP_THRESHOLD = 150;
267
540
  var VOLATILITY_THRESHOLD = 25;
268
541
 
269
542
  // src/core/analysis.ts
543
+ var CAUSAL_MARGIN = 0.05;
544
+ var isEventDriven = (label, graph) => {
545
+ for (const [parent, targets] of graph.entries()) {
546
+ if (parent.startsWith("Event_Tick_") && targets.has(label)) {
547
+ return true;
548
+ }
549
+ }
550
+ return false;
551
+ };
270
552
  var calculateAllSimilarities = (entryA, entryB) => {
271
- const sync = calculateSimilarityCircular(entryA.meta.buffer, entryA.meta.head, entryB.meta.buffer, entryB.meta.head, 0);
272
- const bA = calculateSimilarityCircular(entryA.meta.buffer, entryA.meta.head, entryB.meta.buffer, entryB.meta.head, 1);
273
- const aB = calculateSimilarityCircular(entryA.meta.buffer, entryA.meta.head, entryB.meta.buffer, entryB.meta.head, -1);
553
+ const sync = calculateSimilarityCircular(
554
+ entryA.meta.buffer,
555
+ entryA.meta.head,
556
+ entryB.meta.buffer,
557
+ entryB.meta.head,
558
+ 0
559
+ );
560
+ const bA = calculateSimilarityCircular(
561
+ entryA.meta.buffer,
562
+ entryA.meta.head,
563
+ entryB.meta.buffer,
564
+ entryB.meta.head,
565
+ 1
566
+ );
567
+ const aB = calculateSimilarityCircular(
568
+ entryA.meta.buffer,
569
+ entryA.meta.head,
570
+ entryB.meta.buffer,
571
+ entryB.meta.head,
572
+ -1
573
+ );
274
574
  return { sync, bA, aB, max: Math.max(sync, bA, aB) };
275
575
  };
276
576
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
@@ -278,49 +578,78 @@ var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
278
578
  if (dirtyLabels2.has(entryB.label) && entryA.label > entryB.label) return true;
279
579
  return false;
280
580
  };
281
- var detectRedundancy = (entryA, entryB, similarities, redundantSet) => {
581
+ var pushViolation = (map, source, detail) => {
582
+ if (!map.has(source)) {
583
+ map.set(source, []);
584
+ }
585
+ const list = map.get(source);
586
+ const exists = list.some(
587
+ (v) => v.type === detail.type && v.target === detail.target
588
+ );
589
+ if (!exists) {
590
+ list.push(detail);
591
+ }
592
+ };
593
+ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap) => {
282
594
  const roleA = entryA.meta.role;
283
595
  const roleB = entryB.meta.role;
284
596
  if (roleA === "context" /* CONTEXT */ && roleB === "context" /* CONTEXT */) return;
285
597
  if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
286
598
  if (roleA === "local" /* LOCAL */ && roleB === "context" /* CONTEXT */) {
287
599
  redundantSet.add(entryA.label);
600
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
288
601
  displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
289
602
  } else if (roleA === "context" /* CONTEXT */ && roleB === "local" /* LOCAL */) {
290
603
  redundantSet.add(entryB.label);
604
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
291
605
  displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
292
606
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
293
607
  redundantSet.add(entryA.label);
294
608
  redundantSet.add(entryB.label);
609
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
610
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
295
611
  displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
296
612
  }
297
613
  };
298
- var detectCausalLeak = (entryA, entryB, similarities) => {
614
+ var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
299
615
  if (entryA.isVolatile || entryB.isVolatile) return;
616
+ if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
617
+ const addLeak = (source, target) => {
618
+ if (isEventDriven(target, graph)) return;
619
+ if (!violationMap.has(source)) {
620
+ violationMap.set(source, []);
621
+ }
622
+ violationMap.get(source).push({ type: "causal_leak", target });
623
+ const sourceEntry = source === entryA.label ? entryA : entryB;
624
+ const targetEntry = source === entryA.label ? entryB : entryA;
625
+ displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
626
+ };
300
627
  if (similarities.bA === similarities.max) {
301
- displayCausalHint(entryB.label, entryB.meta, entryA.label, entryA.meta);
628
+ addLeak(entryA.label, entryB.label);
302
629
  } else if (similarities.aB === similarities.max) {
303
- displayCausalHint(entryA.label, entryA.meta, entryB.label, entryB.meta);
630
+ addLeak(entryB.label, entryA.label);
304
631
  }
305
632
  };
306
- var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2) => {
633
+ var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
307
634
  let compCount = 0;
635
+ const violationMap = /* @__PURE__ */ new Map();
308
636
  for (const entryA of dirtyEntries) {
309
637
  for (const entryB of allEntries) {
310
638
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
311
639
  compCount++;
312
640
  const similarities = calculateAllSimilarities(entryA, entryB);
313
641
  if (similarities.max > SIMILARITY_THRESHOLD) {
314
- detectRedundancy(entryA, entryB, similarities, redundantSet);
315
- detectCausalLeak(entryA, entryB, similarities);
642
+ detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
643
+ detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
316
644
  }
317
645
  }
318
646
  }
319
- return compCount;
647
+ return { compCount, violationMap };
320
648
  };
321
649
 
322
650
  // src/engine.ts
323
651
  var BASIS_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("__basis_engine_instance__");
652
+ var EVENT_TTL = 1e4;
324
653
  var NULL_SIGNAL = {
325
654
  role: "proj" /* PROJECTION */,
326
655
  buffer: new Uint8Array(0),
@@ -328,6 +657,30 @@ var NULL_SIGNAL = {
328
657
  density: 0,
329
658
  options: {}
330
659
  };
660
+ var activeEventId = null;
661
+ var activeEventTimer = null;
662
+ var pruneGraph = () => {
663
+ const now = Date.now();
664
+ for (const source of instance.graph.keys()) {
665
+ if (source.startsWith("Event_Tick_")) {
666
+ const parts = source.split("_");
667
+ const timestamp = parseInt(parts[2], 10);
668
+ if (now - timestamp > EVENT_TTL) {
669
+ instance.graph.delete(source);
670
+ }
671
+ }
672
+ }
673
+ };
674
+ var getEventId = () => {
675
+ if (!activeEventId) {
676
+ activeEventId = `Event_Tick_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
677
+ if (activeEventTimer) clearTimeout(activeEventTimer);
678
+ activeEventTimer = setTimeout(() => {
679
+ activeEventId = null;
680
+ }, 0);
681
+ }
682
+ return activeEventId;
683
+ };
331
684
  var getGlobalInstance = () => {
332
685
  const root = globalThis;
333
686
  if (!root[BASIS_INSTANCE_KEY]) {
@@ -340,7 +693,10 @@ var getGlobalInstance = () => {
340
693
  tick: 0,
341
694
  isBatching: false,
342
695
  currentEffectSource: null,
696
+ lastStateUpdate: null,
343
697
  pausedVariables: /* @__PURE__ */ new Set(),
698
+ graph: /* @__PURE__ */ new Map(),
699
+ violationMap: /* @__PURE__ */ new Map(),
344
700
  metrics: {
345
701
  lastAnalysisTimeMs: 0,
346
702
  comparisonCount: 0,
@@ -370,6 +726,14 @@ var calculateTickEntropy = (tickIdx) => {
370
726
  });
371
727
  return 1 - activeCount / total;
372
728
  };
729
+ var recordEdge = (source, target) => {
730
+ if (!source || !target || source === target) return;
731
+ if (!instance.graph.has(source)) {
732
+ instance.graph.set(source, /* @__PURE__ */ new Map());
733
+ }
734
+ const targets = instance.graph.get(source);
735
+ targets.set(target, (targets.get(target) || 0) + 1);
736
+ };
373
737
  var analyzeBasis = () => {
374
738
  if (!instance.config.debug || dirtyLabels.size === 0) {
375
739
  return;
@@ -400,16 +764,33 @@ var analyzeBasis = () => {
400
764
  nextRedundant.add(l);
401
765
  }
402
766
  });
403
- const compCount = detectSubspaceOverlap(
767
+ const { compCount, violationMap } = detectSubspaceOverlap(
404
768
  dirtyEntries,
405
769
  allEntries,
406
770
  nextRedundant,
407
- snapshot
771
+ snapshot,
772
+ instance.graph
408
773
  );
409
774
  instance.redundantLabels.clear();
410
775
  nextRedundant.forEach((l) => {
411
776
  instance.redundantLabels.add(l);
412
777
  });
778
+ violationMap.forEach((newList, label) => {
779
+ const existing = instance.violationMap.get(label) || [];
780
+ newList.forEach((detail) => {
781
+ const alreadyExists = existing.some(
782
+ (v) => v.type === detail.type && v.target === detail.target
783
+ );
784
+ if (!alreadyExists) {
785
+ existing.push(detail);
786
+ }
787
+ });
788
+ instance.violationMap.set(label, existing);
789
+ });
790
+ if (instance.violationMap.size > 500) {
791
+ const keys = Array.from(instance.violationMap.keys()).slice(0, 200);
792
+ keys.forEach((k) => instance.violationMap.delete(k));
793
+ }
413
794
  instance.metrics.lastAnalysisTimeMs = performance.now() - analysisStart;
414
795
  instance.metrics.comparisonCount = compCount;
415
796
  instance.metrics.lastAnalysisTimestamp = Date.now();
@@ -431,6 +812,7 @@ var processHeartbeat = () => {
431
812
  instance.currentTickBatch.clear();
432
813
  currentTickRegistry = {};
433
814
  instance.isBatching = false;
815
+ instance.lastStateUpdate = null;
434
816
  if (dirtyLabels.size > 0) {
435
817
  analyzeBasis();
436
818
  }
@@ -442,6 +824,7 @@ var recordUpdate = (label) => {
442
824
  if (now - instance.lastCleanup > 1e3) {
443
825
  instance.loopCounters.clear();
444
826
  instance.lastCleanup = now;
827
+ pruneGraph();
445
828
  }
446
829
  const count = (instance.loopCounters.get(label) || 0) + 1;
447
830
  instance.loopCounters.set(label, count);
@@ -450,17 +833,25 @@ var recordUpdate = (label) => {
450
833
  instance.pausedVariables.add(label);
451
834
  return false;
452
835
  }
453
- if (instance.currentEffectSource && instance.currentEffectSource !== label) {
454
- const targetMeta = instance.history.get(label);
455
- const sourceMeta = instance.history.get(instance.currentEffectSource);
456
- if (targetMeta) {
457
- const sourceDensity = sourceMeta?.density || 0;
458
- const isVolatile = targetMeta.density > VOLATILITY_THRESHOLD || sourceDensity > VOLATILITY_THRESHOLD;
459
- if (!isVolatile) {
460
- displayCausalHint(label, targetMeta, instance.currentEffectSource, sourceMeta || NULL_SIGNAL);
836
+ const meta = instance.history.get(label);
837
+ let edgeSource = null;
838
+ if (instance.currentEffectSource) {
839
+ edgeSource = instance.currentEffectSource;
840
+ } else {
841
+ edgeSource = getEventId();
842
+ }
843
+ if (edgeSource && edgeSource !== label) {
844
+ recordEdge(edgeSource, label);
845
+ if (instance.currentEffectSource && instance.currentEffectSource !== label) {
846
+ const sourceMeta = instance.history.get(instance.currentEffectSource) || NULL_SIGNAL;
847
+ if (meta && sourceMeta && meta.density < VOLATILITY_THRESHOLD && sourceMeta.density < VOLATILITY_THRESHOLD) {
848
+ displayCausalHint(label, meta, instance.currentEffectSource, sourceMeta);
461
849
  }
462
850
  }
463
851
  }
852
+ if (meta && meta.role === "local" /* LOCAL */ && !instance.currentEffectSource) {
853
+ instance.lastStateUpdate = label;
854
+ }
464
855
  if (currentTickRegistry[label]) return true;
465
856
  currentTickRegistry[label] = true;
466
857
  instance.currentTickBatch.add(label);
@@ -504,10 +895,10 @@ var endEffectTracking = () => {
504
895
  };
505
896
  var printBasisHealthReport = (threshold = 0.5) => {
506
897
  if (!instance.config.debug) return;
507
- displayHealthReport(instance.history, threshold);
898
+ displayHealthReport(instance.history, threshold, instance.violationMap);
508
899
  };
509
900
  var getBasisMetrics = () => ({
510
- engine: "v0.5.x",
901
+ engine: "v0.6.x",
511
902
  hooks: instance.history.size,
512
903
  analysis_ms: instance.metrics.lastAnalysisTimeMs.toFixed(3),
513
904
  entropy: instance.metrics.systemEntropy.toFixed(3)
@@ -834,7 +1225,11 @@ var HUDHeader = ({ isExpanded }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)
834
1225
  var import_jsx_runtime2 = require("react/jsx-runtime");
835
1226
  var BasisContext = (0, import_react3.createContext)({ debug: false });
836
1227
  var isWeb2 = typeof window !== "undefined" && typeof window.document !== "undefined";
837
- var BasisProvider = ({ children, debug = true }) => {
1228
+ var BasisProvider = ({
1229
+ children,
1230
+ debug = true,
1231
+ showHUD = true
1232
+ }) => {
838
1233
  (0, import_react3.useLayoutEffect)(() => {
839
1234
  configureBasis({ debug });
840
1235
  if (isWeb2) {
@@ -843,7 +1238,7 @@ var BasisProvider = ({ children, debug = true }) => {
843
1238
  }, [debug]);
844
1239
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(BasisContext.Provider, { value: { debug }, children: [
845
1240
  children,
846
- debug && isWeb2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BasisHUD, {})
1241
+ debug && showHUD && isWeb2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BasisHUD, {})
847
1242
  ] });
848
1243
  };
849
1244
  var useBasisConfig = () => (0, import_react3.useContext)(BasisContext);