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