react-state-basis 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/{chunk-6GBZSOB3.mjs → chunk-WMHY2C6D.mjs} +300 -86
- package/dist/chunk-WMHY2C6D.mjs.map +1 -0
- package/dist/index.d.mts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +300 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/zustand.js +296 -84
- package/dist/integrations/zustand.js.map +1 -1
- package/dist/integrations/zustand.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-6GBZSOB3.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -192,6 +192,64 @@ The report can include:
|
|
|
192
192
|
|
|
193
193
|
These metrics are diagnostic rather than a score for the quality of your application.
|
|
194
194
|
|
|
195
|
+
### Causal graph
|
|
196
|
+
|
|
197
|
+
`printBasisReport()` gives a diagnosis. If you want to see the evidence that report is based on, Basis also exposes the observed update graph directly.
|
|
198
|
+
|
|
199
|
+
`printBasisGraph()` does nothing unless `debug` is on (same as `printBasisReport()`). `getBasisGraph()` always returns the current snapshot, regardless of `debug`, since it's just serializing the graph, not printing anything.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
window.printBasisGraph()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
prints it to the console, grouped by source:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
📊 BASIS | CAUSAL GRAPH 7 nodes · 7 edges · 2 sources · buffer window 50
|
|
209
|
+
parent → child = observed cause → update. (×N) = times in this window.
|
|
210
|
+
⚡ Event · 3 targets · ×2
|
|
211
|
+
BooleanEntanglement.tsx → isLoading redundant
|
|
212
|
+
BooleanEntanglement.tsx → isSuccess redundant
|
|
213
|
+
BooleanEntanglement.tsx → hasData redundant
|
|
214
|
+
↯ WeatherLab.tsx → effect @ L7
|
|
215
|
+
WeatherLab.tsx → fahrenheit (×2)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For the raw data, either from the console:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
window.getBasisGraph()
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
or imported directly, if you're building your own tooling on top of it rather than reading it from the console:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { getBasisGraph } from 'react-state-basis';
|
|
228
|
+
import type { BasisGraphJSON } from 'react-state-basis';
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Either way it returns:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
{
|
|
235
|
+
generatedAt: number;
|
|
236
|
+
bufferWindowSize: number;
|
|
237
|
+
eventTtlMs: number;
|
|
238
|
+
nodes: { id, name, file, role, density, redundant }[];
|
|
239
|
+
edges: { source, target, weight }[];
|
|
240
|
+
eventGroups: { sourceIds, occurrences, edges }[];
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A few things worth knowing about the shape:
|
|
245
|
+
|
|
246
|
+
* `role` distinguishes real state (`local` / `context` / `store` / `proj`) from `effect` sources, virtual `event` triggers, and `unknown` (a graph edge with no recognized shape - e.g. a custom integration recording edges directly). `effect`, `event`, and `unknown` all have `density: null` - none of them has a real update history of its own.
|
|
247
|
+
* `bufferWindowSize` and `eventTtlMs` are two different clocks: state nodes' `density` is measured over the last `bufferWindowSize` ticks, while virtual `event` nodes are pruned after `eventTtlMs` of inactivity. They're unrelated, so don't read one as describing the other.
|
|
248
|
+
* User interactions are recorded as virtual, per-frame `event` triggers. Repeated interactions that produce the exact same fan-out (same targets, same weights) are collapsed into one entry in `eventGroups`, so clicking the same button 20 times doesn't produce 20 near-identical entries. `nodes` and `edges` remain the full, ungrouped data if you need it - the node/edge counts in the header above are always raw counts, not the number of lines shown after grouping.
|
|
249
|
+
* Only nodes that appear on at least one edge are included - a registered variable that has never caused or received an update won't show up.
|
|
250
|
+
|
|
251
|
+
This is meant for inspecting what Basis actually observed, building your own tooling on top of it, or attaching to a bug report - not as a second diagnostic layer alongside `printBasisReport()`.
|
|
252
|
+
|
|
195
253
|
### Runtime metrics
|
|
196
254
|
|
|
197
255
|
You can inspect engine metrics with:
|
|
@@ -327,4 +385,4 @@ For a deeper look at the implementation and underlying model:
|
|
|
327
385
|
|
|
328
386
|
Built by [LP](https://github.com/liovic) • [MIT License](https://opensource.org/licenses/MIT)
|
|
329
387
|
|
|
330
|
-
</div>
|
|
388
|
+
</div>
|
|
@@ -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
|
|
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
|
-
|
|
5
|
-
const
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
82
|
+
return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
|
|
19
83
|
};
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
@@ -61,13 +120,27 @@ var calculateSpectralInfluence = (graph, maxIterations = 20, tolerance = 1e-3) =
|
|
|
61
120
|
}
|
|
62
121
|
return scores;
|
|
63
122
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
var groupEventSources = (nodes, edges) => {
|
|
124
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
125
|
+
edges.forEach((e) => {
|
|
126
|
+
if (!outgoing.has(e.source)) outgoing.set(e.source, []);
|
|
127
|
+
outgoing.get(e.source).push(e);
|
|
128
|
+
});
|
|
129
|
+
const eventSourceIds = nodes.filter((n) => n.role === "event").map((n) => n.id);
|
|
130
|
+
const signatureOf = (sourceEdges) => sourceEdges.map((e) => `${e.target}@${e.weight}`).sort().join("|");
|
|
131
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
132
|
+
eventSourceIds.forEach((id) => {
|
|
133
|
+
const sig = signatureOf(outgoing.get(id) || []);
|
|
134
|
+
if (!buckets.has(sig)) buckets.set(sig, []);
|
|
135
|
+
buckets.get(sig).push(id);
|
|
136
|
+
});
|
|
137
|
+
const groups = Array.from(buckets.values()).map((sourceIds) => ({
|
|
138
|
+
sourceIds,
|
|
139
|
+
occurrences: sourceIds.length,
|
|
140
|
+
edges: (outgoing.get(sourceIds[0]) || []).slice()
|
|
141
|
+
}));
|
|
142
|
+
return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
|
|
143
|
+
};
|
|
71
144
|
|
|
72
145
|
// src/core/label.ts
|
|
73
146
|
var stripInstance = (label) => {
|
|
@@ -80,6 +153,7 @@ var parseLabel = (label) => {
|
|
|
80
153
|
return { file: parts[0] || "Unknown", name: parts[1] || base };
|
|
81
154
|
};
|
|
82
155
|
var isSameField = (labelA, labelB) => stripInstance(labelA) === stripInstance(labelB);
|
|
156
|
+
var isEffectLabel = (name) => /^effect_L\d+(:\d+)?$/.test(name) || name === "anonymous_effect" || name === "anonymous_layout_effect";
|
|
83
157
|
|
|
84
158
|
// src/core/ranker.ts
|
|
85
159
|
var identifyTopIssues = (graph, history2, redundantLabels2, violationMap) => {
|
|
@@ -181,32 +255,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
|
|
|
181
255
|
var LOG_COOLDOWN = 3e3;
|
|
182
256
|
var THEME = {
|
|
183
257
|
identity: "#6C5CE7",
|
|
184
|
-
// Purple (Brand)
|
|
185
258
|
problem: "#D63031",
|
|
186
|
-
// Red (Bugs)
|
|
187
259
|
solution: "#FBC531",
|
|
188
|
-
// Yellow (Fixes)
|
|
189
260
|
context: "#0984E3",
|
|
190
|
-
// Blue (Locations)
|
|
191
261
|
muted: "#9AA0A6",
|
|
192
|
-
// Gray (Metadata)
|
|
193
262
|
border: "#2E2E35",
|
|
194
263
|
success: "#00b894"
|
|
195
|
-
// Green (Good Score)
|
|
196
264
|
};
|
|
197
265
|
var STYLES = {
|
|
198
|
-
// Structure
|
|
199
266
|
basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
|
|
200
267
|
headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
|
|
201
268
|
headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
|
|
202
269
|
version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
|
|
203
|
-
// Actions
|
|
204
270
|
actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
|
|
205
271
|
actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
|
|
206
|
-
// Context
|
|
207
272
|
impactLabel: `color: ${THEME.context}; font-weight: bold;`,
|
|
208
273
|
location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
|
|
209
|
-
// Text
|
|
210
274
|
subText: `color: ${THEME.muted}; font-size: 11px;`,
|
|
211
275
|
bold: "font-weight: bold;",
|
|
212
276
|
label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
|
|
@@ -221,6 +285,15 @@ var shouldLog = (key) => {
|
|
|
221
285
|
return false;
|
|
222
286
|
};
|
|
223
287
|
var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
|
|
288
|
+
var areSyncSignificant = (metaA, metaB) => {
|
|
289
|
+
const { kSync, densityA, densityB } = countOverlapsCircular(
|
|
290
|
+
metaA.buffer,
|
|
291
|
+
metaA.head,
|
|
292
|
+
metaB.buffer,
|
|
293
|
+
metaB.head
|
|
294
|
+
);
|
|
295
|
+
return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
|
|
296
|
+
};
|
|
224
297
|
var getSuggestedFix = (issue, info) => {
|
|
225
298
|
if (issue.label.includes("Global Event")) {
|
|
226
299
|
return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
|
|
@@ -250,7 +323,7 @@ var getSuggestedFix = (issue, info) => {
|
|
|
250
323
|
}
|
|
251
324
|
return `Check the dependency chain of ${info.name}.`;
|
|
252
325
|
};
|
|
253
|
-
var displayHealthReport = (history2,
|
|
326
|
+
var displayHealthReport = (history2, violationMap) => {
|
|
254
327
|
if (!isWeb) return;
|
|
255
328
|
const entries = Array.from(history2.entries());
|
|
256
329
|
if (entries.length === 0) return;
|
|
@@ -326,11 +399,10 @@ var displayHealthReport = (history2, threshold, violationMap) => {
|
|
|
326
399
|
processed.add(labelA);
|
|
327
400
|
entries.forEach(([labelB, metaB]) => {
|
|
328
401
|
if (labelA === labelB || processed.has(labelB)) return;
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
402
|
+
if (!areSyncSignificant(metaA, metaB)) return;
|
|
403
|
+
if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
|
|
404
|
+
currentCluster.push(labelB);
|
|
405
|
+
processed.add(labelB);
|
|
334
406
|
});
|
|
335
407
|
if (currentCluster.length > 1) clusters.push(currentCluster);
|
|
336
408
|
else independentCount++;
|
|
@@ -406,7 +478,7 @@ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
|
|
|
406
478
|
const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
|
|
407
479
|
console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
|
|
408
480
|
console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
|
|
409
|
-
console.log(`%cIssue:%c ${infoA.name} and ${infoB.name}
|
|
481
|
+
console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} overlapped on ${(sim * 100).toFixed(0)}% of aligned updates.`, STYLES.bold, "");
|
|
410
482
|
if (isContextMirror || isStoreMirror) {
|
|
411
483
|
const sourceType = isStoreMirror ? "External Store" : "Global Context";
|
|
412
484
|
console.log(
|
|
@@ -475,6 +547,99 @@ var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
|
|
|
475
547
|
}
|
|
476
548
|
console.groupEnd();
|
|
477
549
|
};
|
|
550
|
+
var splitHookLine = (raw) => {
|
|
551
|
+
const m = raw.match(/^(.*):(\d+)$/);
|
|
552
|
+
if (!m) return { hook: raw };
|
|
553
|
+
return { hook: m[1], line: Number(m[2]) };
|
|
554
|
+
};
|
|
555
|
+
var formatHook = (raw) => {
|
|
556
|
+
const { hook } = splitHookLine(raw);
|
|
557
|
+
if (!isEffectLabel(hook)) return hook;
|
|
558
|
+
const lineMatch = hook.match(/L(\d+)$/);
|
|
559
|
+
return lineMatch ? `effect @ L${lineMatch[1]}` : "effect (anonymous)";
|
|
560
|
+
};
|
|
561
|
+
var formatNode = (node, fallbackId = "?") => {
|
|
562
|
+
if (!node) return fallbackId;
|
|
563
|
+
if (node.role === "event") return "Event";
|
|
564
|
+
const hook = formatHook(node.name || node.id);
|
|
565
|
+
if (node.file && hook) return `${node.file} \u2192 ${hook}`;
|
|
566
|
+
return hook || node.id;
|
|
567
|
+
};
|
|
568
|
+
var displayGraphReport = (graph) => {
|
|
569
|
+
if (!isWeb) return;
|
|
570
|
+
if (graph.nodes.length === 0) {
|
|
571
|
+
console.log(
|
|
572
|
+
`%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
|
|
573
|
+
STYLES.headerIdentity,
|
|
574
|
+
`color: ${THEME.muted}; font-style: italic;`
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
579
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
580
|
+
graph.edges.forEach((e) => {
|
|
581
|
+
if (!outgoing.has(e.source)) outgoing.set(e.source, []);
|
|
582
|
+
outgoing.get(e.source).push(e);
|
|
583
|
+
});
|
|
584
|
+
const eventGroups = graph.eventGroups.map((g) => ({
|
|
585
|
+
sourceIds: g.sourceIds,
|
|
586
|
+
sourceNode: nodeById.get(g.sourceIds[0]),
|
|
587
|
+
edges: g.edges,
|
|
588
|
+
occurrences: g.occurrences
|
|
589
|
+
}));
|
|
590
|
+
const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
|
|
591
|
+
const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({ sourceIds: [id], sourceNode: nodeById.get(id), edges: outgoing.get(id), occurrences: 1 }));
|
|
592
|
+
const groups = [...eventGroups, ...nonEventGroups].sort(
|
|
593
|
+
(a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
|
|
594
|
+
);
|
|
595
|
+
console.group(
|
|
596
|
+
`%c \u{1F4CA} BASIS | CAUSAL GRAPH %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 buffer window ${graph.bufferWindowSize}`,
|
|
597
|
+
STYLES.headerIdentity,
|
|
598
|
+
`color: ${THEME.muted}; font-weight: normal; font-style: italic;`
|
|
599
|
+
);
|
|
600
|
+
console.log(
|
|
601
|
+
`%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
|
|
602
|
+
STYLES.subText
|
|
603
|
+
);
|
|
604
|
+
groups.forEach((group) => {
|
|
605
|
+
const isEvent = group.sourceNode?.role === "event";
|
|
606
|
+
const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
|
|
607
|
+
const isFx = group.sourceNode?.role === "effect";
|
|
608
|
+
const isUnknown = group.sourceNode?.role === "unknown";
|
|
609
|
+
const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
|
|
610
|
+
const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
|
|
611
|
+
const fanout = group.edges.length;
|
|
612
|
+
const hits = group.occurrences;
|
|
613
|
+
const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
|
|
614
|
+
const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
|
|
615
|
+
console.groupCollapsed(
|
|
616
|
+
`%c${icon} %c${title}`,
|
|
617
|
+
`color: ${color};`,
|
|
618
|
+
"font-family: monospace; font-weight: 600;"
|
|
619
|
+
);
|
|
620
|
+
group.edges.slice().sort((a, b) => b.weight - a.weight).forEach((edge) => {
|
|
621
|
+
const target = nodeById.get(edge.target);
|
|
622
|
+
const label = formatNode(target, edge.target);
|
|
623
|
+
const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
|
|
624
|
+
if (target?.redundant) {
|
|
625
|
+
console.log(
|
|
626
|
+
`%c ${label}%c${weight} %credundant`,
|
|
627
|
+
`color: ${THEME.muted}; font-family: monospace;`,
|
|
628
|
+
`color: ${THEME.muted}; font-style: italic;`,
|
|
629
|
+
`color: ${THEME.problem}; font-weight: bold;`
|
|
630
|
+
);
|
|
631
|
+
} else {
|
|
632
|
+
console.log(
|
|
633
|
+
`%c ${label}%c${weight}`,
|
|
634
|
+
`color: ${THEME.muted}; font-family: monospace;`,
|
|
635
|
+
`color: ${THEME.muted}; font-style: italic;`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
console.groupEnd();
|
|
640
|
+
});
|
|
641
|
+
console.groupEnd();
|
|
642
|
+
};
|
|
478
643
|
var displayViolentBreaker = (label, count, threshold) => {
|
|
479
644
|
if (!isWeb) return;
|
|
480
645
|
const { name } = parseLabel(label);
|
|
@@ -491,7 +656,6 @@ var displayBootLog = (windowSize) => {
|
|
|
491
656
|
};
|
|
492
657
|
|
|
493
658
|
// src/core/analysis.ts
|
|
494
|
-
var CAUSAL_MARGIN = 0.05;
|
|
495
659
|
var isEventDriven = (label, graph) => {
|
|
496
660
|
for (const [parent, targets] of graph.entries()) {
|
|
497
661
|
if (parent.startsWith("Event_Tick_") && targets.has(label)) {
|
|
@@ -501,28 +665,33 @@ var isEventDriven = (label, graph) => {
|
|
|
501
665
|
return false;
|
|
502
666
|
};
|
|
503
667
|
var calculateAllSimilarities = (entryA, entryB) => {
|
|
504
|
-
const
|
|
505
|
-
entryA.meta.buffer,
|
|
506
|
-
entryA.meta.head,
|
|
507
|
-
entryB.meta.buffer,
|
|
508
|
-
entryB.meta.head,
|
|
509
|
-
0
|
|
510
|
-
);
|
|
511
|
-
const bA = calculateSimilarityCircular(
|
|
668
|
+
const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
|
|
512
669
|
entryA.meta.buffer,
|
|
513
670
|
entryA.meta.head,
|
|
514
671
|
entryB.meta.buffer,
|
|
515
|
-
entryB.meta.head
|
|
516
|
-
1
|
|
672
|
+
entryB.meta.head
|
|
517
673
|
);
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
);
|
|
525
|
-
|
|
674
|
+
const sync = cosineFromOverlap(kSync, densityA, densityB);
|
|
675
|
+
const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
|
|
676
|
+
const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
|
|
677
|
+
const max = Math.max(sync, bA, aB);
|
|
678
|
+
const windowSize = entryA.meta.buffer.length;
|
|
679
|
+
const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
|
|
680
|
+
const kLead = Math.max(kALeadsB, kBLeadsA);
|
|
681
|
+
const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
|
|
682
|
+
return {
|
|
683
|
+
sync,
|
|
684
|
+
bA,
|
|
685
|
+
aB,
|
|
686
|
+
max,
|
|
687
|
+
kSync,
|
|
688
|
+
kALeadsB,
|
|
689
|
+
kBLeadsA,
|
|
690
|
+
densityA,
|
|
691
|
+
densityB,
|
|
692
|
+
significantSync,
|
|
693
|
+
significantLead
|
|
694
|
+
};
|
|
526
695
|
};
|
|
527
696
|
var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
|
|
528
697
|
if (entryA.label === entryB.label) return true;
|
|
@@ -547,52 +716,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
|
|
|
547
716
|
const roleA = entryA.meta.role;
|
|
548
717
|
const roleB = entryB.meta.role;
|
|
549
718
|
if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
|
|
550
|
-
if (
|
|
719
|
+
if (similarities.densityA < 2 || similarities.densityB < 2) return;
|
|
720
|
+
const score = similarities.sync;
|
|
551
721
|
if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
|
|
552
722
|
redundantSet.add(entryA.label);
|
|
553
|
-
pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity:
|
|
554
|
-
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta,
|
|
723
|
+
pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: score });
|
|
724
|
+
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
|
|
555
725
|
} else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
|
|
556
726
|
redundantSet.add(entryB.label);
|
|
557
|
-
pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity:
|
|
558
|
-
displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta,
|
|
727
|
+
pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: score });
|
|
728
|
+
displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, score);
|
|
559
729
|
} else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
|
|
560
730
|
redundantSet.add(entryA.label);
|
|
561
731
|
redundantSet.add(entryB.label);
|
|
562
|
-
pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity:
|
|
563
|
-
pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity:
|
|
564
|
-
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta,
|
|
732
|
+
pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: score });
|
|
733
|
+
pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: score });
|
|
734
|
+
displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
|
|
565
735
|
}
|
|
566
736
|
};
|
|
567
737
|
var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
|
|
568
738
|
if (entryA.isVolatile || entryB.isVolatile) return;
|
|
569
|
-
if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
|
|
570
739
|
const addLeak = (source, target) => {
|
|
571
740
|
if (isEventDriven(target, graph)) return;
|
|
572
|
-
|
|
573
|
-
violationMap.set(source, []);
|
|
574
|
-
}
|
|
575
|
-
violationMap.get(source).push({ type: "causal_leak", target });
|
|
741
|
+
pushViolation(violationMap, source, { type: "causal_leak", target });
|
|
576
742
|
const sourceEntry = source === entryA.label ? entryA : entryB;
|
|
577
743
|
const targetEntry = source === entryA.label ? entryB : entryA;
|
|
578
744
|
displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
|
|
579
745
|
};
|
|
580
|
-
if (similarities.
|
|
746
|
+
if (similarities.kALeadsB >= similarities.kBLeadsA) {
|
|
581
747
|
addLeak(entryA.label, entryB.label);
|
|
582
|
-
} else
|
|
748
|
+
} else {
|
|
583
749
|
addLeak(entryB.label, entryA.label);
|
|
584
750
|
}
|
|
585
751
|
};
|
|
586
752
|
var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
|
|
587
|
-
let compCount = 0;
|
|
588
753
|
const violationMap = /* @__PURE__ */ new Map();
|
|
754
|
+
let compCount = 0;
|
|
589
755
|
for (const entryA of dirtyEntries) {
|
|
590
756
|
for (const entryB of allEntries) {
|
|
591
757
|
if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
|
|
592
758
|
compCount++;
|
|
593
759
|
const similarities = calculateAllSimilarities(entryA, entryB);
|
|
594
|
-
if (similarities.
|
|
760
|
+
if (similarities.significantSync) {
|
|
595
761
|
detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
|
|
762
|
+
}
|
|
763
|
+
if (similarities.significantLead) {
|
|
596
764
|
detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
|
|
597
765
|
}
|
|
598
766
|
}
|
|
@@ -851,9 +1019,9 @@ var beginEffectTracking = (l) => {
|
|
|
851
1019
|
var endEffectTracking = () => {
|
|
852
1020
|
instance.currentEffectSource = null;
|
|
853
1021
|
};
|
|
854
|
-
var printBasisHealthReport = (
|
|
1022
|
+
var printBasisHealthReport = () => {
|
|
855
1023
|
if (!instance.config.debug) return;
|
|
856
|
-
displayHealthReport(instance.history,
|
|
1024
|
+
displayHealthReport(instance.history, instance.violationMap);
|
|
857
1025
|
};
|
|
858
1026
|
var getBasisMetrics = () => ({
|
|
859
1027
|
engine: "v0.6.x",
|
|
@@ -861,9 +1029,53 @@ var getBasisMetrics = () => ({
|
|
|
861
1029
|
analysis_ms: instance.metrics.lastAnalysisTimeMs.toFixed(3),
|
|
862
1030
|
entropy: instance.metrics.systemEntropy.toFixed(3)
|
|
863
1031
|
});
|
|
1032
|
+
var getBasisGraph = () => {
|
|
1033
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
1034
|
+
const edges = [];
|
|
1035
|
+
instance.graph.forEach((targets, source) => {
|
|
1036
|
+
nodeIds.add(source);
|
|
1037
|
+
targets.forEach((weight, target) => {
|
|
1038
|
+
nodeIds.add(target);
|
|
1039
|
+
edges.push({ source, target, weight });
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
const nodes = Array.from(nodeIds).map((id) => {
|
|
1043
|
+
if (id.startsWith("Event_Tick_")) {
|
|
1044
|
+
return { id, name: "Event", file: "(shared trigger)", role: "event", density: null, redundant: false };
|
|
1045
|
+
}
|
|
1046
|
+
const meta = instance.history.get(id);
|
|
1047
|
+
const { file, name } = parseLabel(id);
|
|
1048
|
+
if (!meta) {
|
|
1049
|
+
const role = isEffectLabel(name) ? "effect" : "unknown";
|
|
1050
|
+
return { id, name, file, role, density: null, redundant: false };
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
id,
|
|
1054
|
+
name,
|
|
1055
|
+
file,
|
|
1056
|
+
role: meta.role,
|
|
1057
|
+
density: meta.density,
|
|
1058
|
+
redundant: instance.redundantLabels.has(id)
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
return {
|
|
1062
|
+
generatedAt: Date.now(),
|
|
1063
|
+
bufferWindowSize: WINDOW_SIZE,
|
|
1064
|
+
eventTtlMs: EVENT_TTL,
|
|
1065
|
+
nodes,
|
|
1066
|
+
edges,
|
|
1067
|
+
eventGroups: groupEventSources(nodes, edges)
|
|
1068
|
+
};
|
|
1069
|
+
};
|
|
1070
|
+
var printBasisGraph = () => {
|
|
1071
|
+
if (!instance.config.debug) return;
|
|
1072
|
+
displayGraphReport(getBasisGraph());
|
|
1073
|
+
};
|
|
864
1074
|
if (typeof window !== "undefined") {
|
|
865
1075
|
window.printBasisReport = printBasisHealthReport;
|
|
866
1076
|
window.getBasisMetrics = getBasisMetrics;
|
|
1077
|
+
window.getBasisGraph = getBasisGraph;
|
|
1078
|
+
window.printBasisGraph = printBasisGraph;
|
|
867
1079
|
}
|
|
868
1080
|
|
|
869
1081
|
export {
|
|
@@ -879,6 +1091,8 @@ export {
|
|
|
879
1091
|
beginEffectTracking,
|
|
880
1092
|
endEffectTracking,
|
|
881
1093
|
printBasisHealthReport,
|
|
882
|
-
getBasisMetrics
|
|
1094
|
+
getBasisMetrics,
|
|
1095
|
+
getBasisGraph,
|
|
1096
|
+
printBasisGraph
|
|
883
1097
|
};
|
|
884
|
-
//# sourceMappingURL=chunk-
|
|
1098
|
+
//# sourceMappingURL=chunk-WMHY2C6D.mjs.map
|