react-state-basis 0.6.3 → 0.6.5
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 +253 -74
- package/dist/{chunk-E3D6YEKB.mjs → chunk-EJXGN76H.mjs} +170 -2
- package/dist/chunk-EJXGN76H.mjs.map +1 -0
- package/dist/client.d.mts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js.map +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +36 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +184 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +19 -2
- package/dist/index.mjs.map +1 -1
- package/dist/integrations/zustand.js +159 -0
- package/dist/integrations/zustand.js.map +1 -1
- package/dist/integrations/zustand.mjs +1 -1
- package/dist/plugin.js +2 -1
- package/dist/production.d.mts +3 -3
- package/dist/production.d.ts +3 -3
- package/dist/production.js +8 -7
- package/dist/production.js.map +1 -1
- package/dist/production.mjs +8 -7
- package/dist/production.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-E3D6YEKB.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
|
|
7
7
|
# react-state-basis
|
|
8
|
-
### Runtime Architectural Auditor for React
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
### Runtime diagnostics for React state
|
|
10
|
+
|
|
11
|
+
**Basis observes when state updates happen and uses those patterns to highlight state-management issues that can be difficult to spot in a code review or profiler. It does not inspect state values.**
|
|
11
12
|
|
|
12
13
|
[](https://www.npmjs.com/package/react-state-basis)
|
|
13
14
|
[](https://github.com/liovic/react-state-basis/stargazers)
|
|
@@ -20,12 +21,16 @@
|
|
|
20
21
|
## Quick Start
|
|
21
22
|
|
|
22
23
|
### 1. Install
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
npm i react-state-basis
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
### 2. Setup
|
|
28
|
-
|
|
29
|
+
### 2. Setup with Vite
|
|
30
|
+
|
|
31
|
+
Add the plugin to your `vite.config.ts`.
|
|
32
|
+
|
|
33
|
+
The Babel plugin labels React hooks automatically, so you can continue importing from `react` as usual.
|
|
29
34
|
|
|
30
35
|
```ts
|
|
31
36
|
import { defineConfig } from 'vite';
|
|
@@ -34,43 +39,55 @@ import { basis } from 'react-state-basis/vite';
|
|
|
34
39
|
|
|
35
40
|
export default defineConfig({
|
|
36
41
|
plugins: [
|
|
37
|
-
react({
|
|
38
|
-
babel: {
|
|
42
|
+
react({
|
|
43
|
+
babel: {
|
|
44
|
+
plugins: [['react-state-basis/plugin']],
|
|
45
|
+
},
|
|
39
46
|
}),
|
|
40
|
-
basis()
|
|
41
|
-
]
|
|
47
|
+
basis(),
|
|
48
|
+
],
|
|
42
49
|
});
|
|
43
50
|
```
|
|
51
|
+
This is the supported setup today. Next.js / SWC is not instrumented yet.
|
|
44
52
|
|
|
45
53
|
### 3. Initialize
|
|
54
|
+
|
|
46
55
|
```tsx
|
|
47
56
|
import { BasisProvider } from 'react-state-basis';
|
|
48
57
|
|
|
49
58
|
root.render(
|
|
50
|
-
<BasisProvider
|
|
59
|
+
<BasisProvider
|
|
51
60
|
debug={true}
|
|
52
|
-
showHUD={true}
|
|
61
|
+
showHUD={true}
|
|
53
62
|
>
|
|
54
63
|
<App />
|
|
55
64
|
</BasisProvider>
|
|
56
65
|
);
|
|
57
66
|
```
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
Set `showHUD={false}` to keep the diagnostics in the console without showing the overlay.
|
|
69
|
+
|
|
70
|
+
### 4. Try it
|
|
71
|
+
|
|
72
|
+
For example:
|
|
61
73
|
|
|
62
74
|
```tsx
|
|
63
75
|
const [a, setA] = useState(0);
|
|
64
76
|
const [b, setB] = useState(0);
|
|
65
77
|
|
|
66
78
|
useEffect(() => {
|
|
67
|
-
setB(a + 1);
|
|
79
|
+
setB(a + 1);
|
|
68
80
|
}, [a]);
|
|
69
81
|
|
|
70
|
-
return
|
|
82
|
+
return (
|
|
83
|
+
<button onClick={() => setA(a + 1)}>
|
|
84
|
+
Update
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
71
87
|
```
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
When the button is clicked, Basis can identify the effect-driven update pattern and report where it originated. You should see in your console:
|
|
90
|
+
|
|
74
91
|
```
|
|
75
92
|
⚡ BASIS | DOUBLE RENDER
|
|
76
93
|
📍 Location: YourComponent.tsx
|
|
@@ -78,64 +95,199 @@ Issue: effect_L5 triggers b in a separate frame.
|
|
|
78
95
|
Fix: Derive b during the render phase (remove effect) or wrap in useMemo.
|
|
79
96
|
```
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
### 5. Control & Scope
|
|
84
|
-
* **Ghost Mode:** Disable the visual overlay while keeping console-based forensics active by setting `showHUD={false}` on the provider.
|
|
85
|
-
* **Selective Auditing:** Add `// @basis-ignore` at the top of any file to disable instrumentation. Recommended for:
|
|
86
|
-
* High-frequency animation logic (>60fps)
|
|
87
|
-
* Third-party library wrappers
|
|
88
|
-
* Intentional synchronization (e.g., local mirrors of external caches)
|
|
98
|
+
Detection happens at runtime and timing varies by pattern.
|
|
89
99
|
|
|
90
100
|
---
|
|
91
101
|
|
|
92
102
|
## HUD
|
|
93
103
|
|
|
94
|
-
The optional
|
|
104
|
+
The optional HUD shows state updates as they happen.
|
|
95
105
|
|
|
96
106
|
<p align="center">
|
|
97
|
-
<img src="./assets/050Basis.gif" width="800" alt="Basis Demo"
|
|
107
|
+
<img src="./assets/050Basis.gif" width="800" alt="Basis Demo">
|
|
98
108
|
</p>
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
The HUD is useful for seeing individual updates. The console report looks at the observed update graph over time, which can reveal patterns that are harder to see from a single interaction.
|
|
101
111
|
|
|
102
112
|
---
|
|
103
113
|
|
|
104
|
-
## What Basis
|
|
114
|
+
## What Basis Looks For
|
|
115
|
+
|
|
116
|
+
Basis does not try to determine whether your state is "correct." Instead, it looks for update patterns that are often worth investigating.
|
|
117
|
+
|
|
118
|
+
### Effect-driven updates
|
|
119
|
+
|
|
120
|
+
A `useEffect` causes another state update immediately after rendering.
|
|
121
|
+
|
|
122
|
+
This can be a sign that some state could be derived during render instead.
|
|
123
|
+
|
|
124
|
+
### Correlated state
|
|
125
|
+
|
|
126
|
+
Two pieces of state repeatedly update within the same time window.
|
|
127
|
+
|
|
128
|
+
For example:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
132
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If these values consistently change together, it may be worth checking whether they could be represented by a single state value.
|
|
136
|
+
|
|
137
|
+
Basis reports the correlation; it does not assume that the states should be merged.
|
|
138
|
+
|
|
139
|
+
### Fragmented updates
|
|
105
140
|
|
|
106
|
-
|
|
141
|
+
A single interaction causes updates across multiple components, files, contexts, or stores.
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
- **⚡ Prime Movers (Likely Root Causes)** - Skips downstream symptoms and points you to the hook or event most likely to have started the chain reaction. When multiple updates fire in the same tick, Basis ranks candidates by their position in the update graph rather than asserting a single definitive cause.
|
|
110
|
-
- **⚡ Fragmented Updates** - A single click forces updates in multiple different files or contexts at once (tearing risk).
|
|
111
|
-
- **Context Mirroring** - You're redundantly copying Global Context data into local state, creating two sources of truth.
|
|
112
|
-
- **Duplicate State** - Two variables always update at the exact same time and should probably be merged (e.g. `isLoading` + `isSuccess`).
|
|
113
|
-
- **🛑 Infinite Loops** - A safety circuit-breaker that kills the auditor before a recursive update freezes your browser.
|
|
143
|
+
Sometimes this is intentional. In other cases, it can indicate that state ownership is spread across several places.
|
|
114
144
|
|
|
115
|
-
|
|
145
|
+
### Context mirroring
|
|
146
|
+
|
|
147
|
+
Local state is repeatedly updated from Context state.
|
|
148
|
+
|
|
149
|
+
This can create two representations of the same information and is worth reviewing when the local copy does not have an independent purpose.
|
|
150
|
+
|
|
151
|
+
### Update origins
|
|
152
|
+
|
|
153
|
+
When several updates occur together, Basis can use the observed update graph to identify which updates appear upstream of others.
|
|
154
|
+
|
|
155
|
+
This is intended to help investigate a chain of updates rather than simply reporting every downstream symptom.
|
|
156
|
+
|
|
157
|
+
### Infinite update protection
|
|
158
|
+
|
|
159
|
+
Basis includes safeguards to stop its own instrumentation from continuing indefinitely when an application enters a recursive update loop.
|
|
116
160
|
|
|
117
161
|
---
|
|
118
162
|
|
|
119
|
-
|
|
163
|
+
### Important: these are signals, not proofs
|
|
164
|
+
|
|
165
|
+
Basis uses runtime timing and correlation heuristics.
|
|
120
166
|
|
|
121
|
-
|
|
122
|
-
Check your entire app's state architecture by running `window.printBasisReport()` in the console.
|
|
167
|
+
A detected pattern is **not automatically a bug**, and Basis does not know the intent behind your application architecture.
|
|
123
168
|
|
|
124
|
-
|
|
125
|
-
* **Efficiency Score:** A rough ratio of independent update sources vs effect-driven follow-up updates. Diagnostic, not a grade.
|
|
126
|
-
* **Sync Issues:** Groups variables that tend to update together into clusters (e.g., boolean pairs that are really one piece of state).
|
|
169
|
+
Use the results as prompts for investigation rather than as rules for how React code should be written.
|
|
127
170
|
|
|
128
|
-
|
|
129
|
-
Verify engine efficiency and heap stability in real-time via `window.getBasisMetrics()`.
|
|
171
|
+
[See examples and possible fixes →](https://github.com/liovic/react-state-basis/wiki/The-Forensic-Catalog)
|
|
130
172
|
|
|
131
173
|
---
|
|
132
174
|
|
|
133
|
-
##
|
|
175
|
+
## Reports
|
|
176
|
+
|
|
177
|
+
Run:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
window.printBasisReport()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
to print a summary of the observed update graph.
|
|
134
184
|
|
|
135
|
-
|
|
185
|
+
The report can include:
|
|
136
186
|
|
|
137
|
-
*
|
|
138
|
-
*
|
|
187
|
+
* **Update sources** - where observed update chains appear to originate.
|
|
188
|
+
* **Fan-out** - which updates are followed by the largest number of downstream updates.
|
|
189
|
+
* **Correlated state** - state variables that repeatedly update together.
|
|
190
|
+
* **Effect-driven updates** - updates that occur as a consequence of effects.
|
|
191
|
+
* **Engine metrics** - runtime measurements collected by the Basis engine.
|
|
192
|
+
|
|
193
|
+
These metrics are diagnostic rather than a score for the quality of your application.
|
|
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
|
+
|
|
253
|
+
### Runtime metrics
|
|
254
|
+
|
|
255
|
+
You can inspect engine metrics with:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
window.getBasisMetrics()
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Controlling the Instrumentation
|
|
264
|
+
|
|
265
|
+
### Console-only mode
|
|
266
|
+
|
|
267
|
+
Disable the HUD while keeping diagnostics enabled:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<BasisProvider showHUD={false}>
|
|
271
|
+
<App />
|
|
272
|
+
</BasisProvider>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Ignoring files
|
|
276
|
+
|
|
277
|
+
Add:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// @basis-ignore
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
to a file to disable Basis instrumentation for that file.
|
|
284
|
+
|
|
285
|
+
This can be useful for:
|
|
286
|
+
|
|
287
|
+
* high-frequency animation code
|
|
288
|
+
* third-party library wrappers
|
|
289
|
+
* intentionally synchronized state
|
|
290
|
+
* code where instrumentation is not useful
|
|
139
291
|
|
|
140
292
|
---
|
|
141
293
|
|
|
@@ -143,62 +295,89 @@ Basis has been tested against industry-standard codebases:
|
|
|
143
295
|
|
|
144
296
|
### Zustand
|
|
145
297
|
|
|
146
|
-
|
|
147
|
-
store updates. Store signals appear in the HUD and health report alongside your React state.
|
|
298
|
+
Basis can observe Zustand store updates alongside React state.
|
|
148
299
|
|
|
149
300
|
```typescript
|
|
150
301
|
import { create } from 'zustand';
|
|
151
302
|
import { basisLogger } from 'react-state-basis/zustand';
|
|
152
303
|
|
|
153
304
|
export const useStore = create(
|
|
154
|
-
basisLogger(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
305
|
+
basisLogger(
|
|
306
|
+
(set) => ({
|
|
307
|
+
theme: 'light',
|
|
308
|
+
|
|
309
|
+
toggleTheme: () =>
|
|
310
|
+
set((state) => ({
|
|
311
|
+
theme: state.theme === 'light' ? 'dark' : 'light',
|
|
312
|
+
})),
|
|
313
|
+
}),
|
|
314
|
+
'MyStore'
|
|
315
|
+
)
|
|
160
316
|
);
|
|
161
317
|
```
|
|
162
318
|
|
|
163
|
-
This
|
|
164
|
-
**Global Event Fragmentation** across React and Zustand state simultaneously.
|
|
319
|
+
This allows React and Zustand updates to appear in the same runtime graph.
|
|
165
320
|
|
|
166
|
-
[See
|
|
321
|
+
[See the Zustand example →](./examples/basis-zustand/)
|
|
167
322
|
|
|
168
|
-
###
|
|
323
|
+
### Planned integrations
|
|
169
324
|
|
|
170
|
-
|
|
325
|
+
XState, React Query, and Redux Toolkit are planned.
|
|
326
|
+
|
|
327
|
+
Community contributions are welcome.
|
|
171
328
|
|
|
172
329
|
---
|
|
173
330
|
|
|
174
331
|
## Performance & Privacy
|
|
175
332
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
**
|
|
333
|
+
Basis is designed primarily as a development-time diagnostic tool.
|
|
334
|
+
|
|
335
|
+
* **Development:** instrumentation overhead is designed to remain small; current benchmarks show less than 1ms per update cycle in tested scenarios.
|
|
336
|
+
* **Production:** monitoring is disabled, with a small production footprint.
|
|
337
|
+
* **Privacy:** Basis records update timing and relationships, not application state values.
|
|
338
|
+
|
|
339
|
+
Actual overhead depends on the application and instrumentation configuration.
|
|
179
340
|
|
|
180
|
-
[
|
|
341
|
+
[See benchmarks →](https://github.com/liovic/react-state-basis/wiki/Performance-Forensics)
|
|
181
342
|
|
|
182
343
|
---
|
|
183
344
|
|
|
184
|
-
##
|
|
345
|
+
## Real-World Examples
|
|
185
346
|
|
|
186
|
-
|
|
347
|
+
Basis has also been tested against existing open-source applications.
|
|
348
|
+
|
|
349
|
+
* **Excalidraw** - Basis identified a theme synchronization pattern and a possible simplification. [PR #10637](https://github.com/excalidraw/excalidraw/pull/10637) was proposed but not merged.
|
|
350
|
+
* **shadcn-admin** - Basis identified a redundant state pattern in viewport detection hooks. [PR #274](https://github.com/satnaing/shadcn-admin/pull/274) was merged.
|
|
351
|
+
|
|
352
|
+
These examples are intended as demonstrations of the tool's output, not as claims that every detected pattern represents a defect.
|
|
187
353
|
|
|
188
354
|
---
|
|
189
355
|
|
|
190
|
-
##
|
|
356
|
+
## How It Works
|
|
357
|
+
|
|
358
|
+
Basis observes the timing and relationships between state updates while your application runs.
|
|
359
|
+
|
|
360
|
+
It builds an in-memory representation of those updates and applies heuristics to identify recurring patterns.
|
|
361
|
+
|
|
362
|
+
It does **not** need to inspect the values stored in your state to perform these checks.
|
|
191
363
|
|
|
192
|
-
|
|
364
|
+
The analysis is intentionally heuristic. Runtime behavior can show that two things consistently happen together, but it cannot by itself prove why they happen together or whether the relationship is intentional.
|
|
193
365
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
366
|
+
For a deeper look at the implementation and underlying model:
|
|
367
|
+
|
|
368
|
+
[Read the documentation and theory →](https://github.com/liovic/react-state-basis/wiki)
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Roadmap
|
|
199
373
|
|
|
374
|
+
* ✓ **v0.4.x** - Identify state that repeatedly updates together
|
|
375
|
+
* ✓ **v0.5.x** - Identify local state synchronized from Context
|
|
376
|
+
* → **v0.6.x** - Analyze update fan-out and likely upstream sources
|
|
377
|
+
* **v0.7.x** - Improve detection of derived vs. independent state
|
|
378
|
+
* **v0.8.x** - Explore how much local state components actually use
|
|
200
379
|
|
|
201
|
-
[
|
|
380
|
+
[See the full roadmap →](https://github.com/liovic/react-state-basis/wiki/Roadmap)
|
|
202
381
|
|
|
203
382
|
---
|
|
204
383
|
|
|
@@ -61,6 +61,27 @@ var calculateSpectralInfluence = (graph, maxIterations = 20, tolerance = 1e-3) =
|
|
|
61
61
|
}
|
|
62
62
|
return scores;
|
|
63
63
|
};
|
|
64
|
+
var groupEventSources = (nodes, edges) => {
|
|
65
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
66
|
+
edges.forEach((e) => {
|
|
67
|
+
if (!outgoing.has(e.source)) outgoing.set(e.source, []);
|
|
68
|
+
outgoing.get(e.source).push(e);
|
|
69
|
+
});
|
|
70
|
+
const eventSourceIds = nodes.filter((n) => n.role === "event").map((n) => n.id);
|
|
71
|
+
const signatureOf = (sourceEdges) => sourceEdges.map((e) => `${e.target}@${e.weight}`).sort().join("|");
|
|
72
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
73
|
+
eventSourceIds.forEach((id) => {
|
|
74
|
+
const sig = signatureOf(outgoing.get(id) || []);
|
|
75
|
+
if (!buckets.has(sig)) buckets.set(sig, []);
|
|
76
|
+
buckets.get(sig).push(id);
|
|
77
|
+
});
|
|
78
|
+
const groups = Array.from(buckets.values()).map((sourceIds) => ({
|
|
79
|
+
sourceIds,
|
|
80
|
+
occurrences: sourceIds.length,
|
|
81
|
+
edges: (outgoing.get(sourceIds[0]) || []).slice()
|
|
82
|
+
}));
|
|
83
|
+
return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
|
|
84
|
+
};
|
|
64
85
|
|
|
65
86
|
// src/core/constants.ts
|
|
66
87
|
var WINDOW_SIZE = 50;
|
|
@@ -80,6 +101,7 @@ var parseLabel = (label) => {
|
|
|
80
101
|
return { file: parts[0] || "Unknown", name: parts[1] || base };
|
|
81
102
|
};
|
|
82
103
|
var isSameField = (labelA, labelB) => stripInstance(labelA) === stripInstance(labelB);
|
|
104
|
+
var isEffectLabel = (name) => /^effect_L\d+(:\d+)?$/.test(name) || name === "anonymous_effect" || name === "anonymous_layout_effect";
|
|
83
105
|
|
|
84
106
|
// src/core/ranker.ts
|
|
85
107
|
var identifyTopIssues = (graph, history2, redundantLabels2, violationMap) => {
|
|
@@ -475,6 +497,99 @@ var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
|
|
|
475
497
|
}
|
|
476
498
|
console.groupEnd();
|
|
477
499
|
};
|
|
500
|
+
var splitHookLine = (raw) => {
|
|
501
|
+
const m = raw.match(/^(.*):(\d+)$/);
|
|
502
|
+
if (!m) return { hook: raw };
|
|
503
|
+
return { hook: m[1], line: Number(m[2]) };
|
|
504
|
+
};
|
|
505
|
+
var formatHook = (raw) => {
|
|
506
|
+
const { hook } = splitHookLine(raw);
|
|
507
|
+
if (!isEffectLabel(hook)) return hook;
|
|
508
|
+
const lineMatch = hook.match(/L(\d+)$/);
|
|
509
|
+
return lineMatch ? `effect @ L${lineMatch[1]}` : "effect (anonymous)";
|
|
510
|
+
};
|
|
511
|
+
var formatNode = (node, fallbackId = "?") => {
|
|
512
|
+
if (!node) return fallbackId;
|
|
513
|
+
if (node.role === "event") return "Event";
|
|
514
|
+
const hook = formatHook(node.name || node.id);
|
|
515
|
+
if (node.file && hook) return `${node.file} \u2192 ${hook}`;
|
|
516
|
+
return hook || node.id;
|
|
517
|
+
};
|
|
518
|
+
var displayGraphReport = (graph) => {
|
|
519
|
+
if (!isWeb) return;
|
|
520
|
+
if (graph.nodes.length === 0) {
|
|
521
|
+
console.log(
|
|
522
|
+
`%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
|
|
523
|
+
STYLES.headerIdentity,
|
|
524
|
+
`color: ${THEME.muted}; font-style: italic;`
|
|
525
|
+
);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
529
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
530
|
+
graph.edges.forEach((e) => {
|
|
531
|
+
if (!outgoing.has(e.source)) outgoing.set(e.source, []);
|
|
532
|
+
outgoing.get(e.source).push(e);
|
|
533
|
+
});
|
|
534
|
+
const eventGroups = graph.eventGroups.map((g) => ({
|
|
535
|
+
sourceIds: g.sourceIds,
|
|
536
|
+
sourceNode: nodeById.get(g.sourceIds[0]),
|
|
537
|
+
edges: g.edges,
|
|
538
|
+
occurrences: g.occurrences
|
|
539
|
+
}));
|
|
540
|
+
const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
|
|
541
|
+
const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({ sourceIds: [id], sourceNode: nodeById.get(id), edges: outgoing.get(id), occurrences: 1 }));
|
|
542
|
+
const groups = [...eventGroups, ...nonEventGroups].sort(
|
|
543
|
+
(a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
|
|
544
|
+
);
|
|
545
|
+
console.group(
|
|
546
|
+
`%c \u{1F4CA} BASIS | CAUSAL GRAPH %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 buffer window ${graph.bufferWindowSize}`,
|
|
547
|
+
STYLES.headerIdentity,
|
|
548
|
+
`color: ${THEME.muted}; font-weight: normal; font-style: italic;`
|
|
549
|
+
);
|
|
550
|
+
console.log(
|
|
551
|
+
`%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
|
|
552
|
+
STYLES.subText
|
|
553
|
+
);
|
|
554
|
+
groups.forEach((group) => {
|
|
555
|
+
const isEvent = group.sourceNode?.role === "event";
|
|
556
|
+
const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
|
|
557
|
+
const isFx = group.sourceNode?.role === "effect";
|
|
558
|
+
const isUnknown = group.sourceNode?.role === "unknown";
|
|
559
|
+
const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
|
|
560
|
+
const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
|
|
561
|
+
const fanout = group.edges.length;
|
|
562
|
+
const hits = group.occurrences;
|
|
563
|
+
const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
|
|
564
|
+
const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
|
|
565
|
+
console.groupCollapsed(
|
|
566
|
+
`%c${icon} %c${title}`,
|
|
567
|
+
`color: ${color};`,
|
|
568
|
+
"font-family: monospace; font-weight: 600;"
|
|
569
|
+
);
|
|
570
|
+
group.edges.slice().sort((a, b) => b.weight - a.weight).forEach((edge) => {
|
|
571
|
+
const target = nodeById.get(edge.target);
|
|
572
|
+
const label = formatNode(target, edge.target);
|
|
573
|
+
const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
|
|
574
|
+
if (target?.redundant) {
|
|
575
|
+
console.log(
|
|
576
|
+
`%c ${label}%c${weight} %credundant`,
|
|
577
|
+
`color: ${THEME.muted}; font-family: monospace;`,
|
|
578
|
+
`color: ${THEME.muted}; font-style: italic;`,
|
|
579
|
+
`color: ${THEME.problem}; font-weight: bold;`
|
|
580
|
+
);
|
|
581
|
+
} else {
|
|
582
|
+
console.log(
|
|
583
|
+
`%c ${label}%c${weight}`,
|
|
584
|
+
`color: ${THEME.muted}; font-family: monospace;`,
|
|
585
|
+
`color: ${THEME.muted}; font-style: italic;`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
console.groupEnd();
|
|
590
|
+
});
|
|
591
|
+
console.groupEnd();
|
|
592
|
+
};
|
|
478
593
|
var displayViolentBreaker = (label, count, threshold) => {
|
|
479
594
|
if (!isWeb) return;
|
|
480
595
|
const { name } = parseLabel(label);
|
|
@@ -837,6 +952,13 @@ var unregisterVariable = (l) => {
|
|
|
837
952
|
instance.loopCounters.delete(l);
|
|
838
953
|
instance.pausedVariables.delete(l);
|
|
839
954
|
instance.redundantLabels.delete(l);
|
|
955
|
+
instance.graph.delete(l);
|
|
956
|
+
instance.graph.forEach((targets) => targets.delete(l));
|
|
957
|
+
instance.violationMap.delete(l);
|
|
958
|
+
instance.violationMap.forEach((list) => {
|
|
959
|
+
const idx = list.findIndex((v) => v.target === l);
|
|
960
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
961
|
+
});
|
|
840
962
|
};
|
|
841
963
|
var beginEffectTracking = (l) => {
|
|
842
964
|
if (instance.config.debug) instance.currentEffectSource = l;
|
|
@@ -854,9 +976,53 @@ var getBasisMetrics = () => ({
|
|
|
854
976
|
analysis_ms: instance.metrics.lastAnalysisTimeMs.toFixed(3),
|
|
855
977
|
entropy: instance.metrics.systemEntropy.toFixed(3)
|
|
856
978
|
});
|
|
979
|
+
var getBasisGraph = () => {
|
|
980
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
981
|
+
const edges = [];
|
|
982
|
+
instance.graph.forEach((targets, source) => {
|
|
983
|
+
nodeIds.add(source);
|
|
984
|
+
targets.forEach((weight, target) => {
|
|
985
|
+
nodeIds.add(target);
|
|
986
|
+
edges.push({ source, target, weight });
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
const nodes = Array.from(nodeIds).map((id) => {
|
|
990
|
+
if (id.startsWith("Event_Tick_")) {
|
|
991
|
+
return { id, name: "Event", file: "(shared trigger)", role: "event", density: null, redundant: false };
|
|
992
|
+
}
|
|
993
|
+
const meta = instance.history.get(id);
|
|
994
|
+
const { file, name } = parseLabel(id);
|
|
995
|
+
if (!meta) {
|
|
996
|
+
const role = isEffectLabel(name) ? "effect" : "unknown";
|
|
997
|
+
return { id, name, file, role, density: null, redundant: false };
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
id,
|
|
1001
|
+
name,
|
|
1002
|
+
file,
|
|
1003
|
+
role: meta.role,
|
|
1004
|
+
density: meta.density,
|
|
1005
|
+
redundant: instance.redundantLabels.has(id)
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
return {
|
|
1009
|
+
generatedAt: Date.now(),
|
|
1010
|
+
bufferWindowSize: WINDOW_SIZE,
|
|
1011
|
+
eventTtlMs: EVENT_TTL,
|
|
1012
|
+
nodes,
|
|
1013
|
+
edges,
|
|
1014
|
+
eventGroups: groupEventSources(nodes, edges)
|
|
1015
|
+
};
|
|
1016
|
+
};
|
|
1017
|
+
var printBasisGraph = () => {
|
|
1018
|
+
if (!instance.config.debug) return;
|
|
1019
|
+
displayGraphReport(getBasisGraph());
|
|
1020
|
+
};
|
|
857
1021
|
if (typeof window !== "undefined") {
|
|
858
1022
|
window.printBasisReport = printBasisHealthReport;
|
|
859
1023
|
window.getBasisMetrics = getBasisMetrics;
|
|
1024
|
+
window.getBasisGraph = getBasisGraph;
|
|
1025
|
+
window.printBasisGraph = printBasisGraph;
|
|
860
1026
|
}
|
|
861
1027
|
|
|
862
1028
|
export {
|
|
@@ -872,6 +1038,8 @@ export {
|
|
|
872
1038
|
beginEffectTracking,
|
|
873
1039
|
endEffectTracking,
|
|
874
1040
|
printBasisHealthReport,
|
|
875
|
-
getBasisMetrics
|
|
1041
|
+
getBasisMetrics,
|
|
1042
|
+
getBasisGraph,
|
|
1043
|
+
printBasisGraph
|
|
876
1044
|
};
|
|
877
|
-
//# sourceMappingURL=chunk-
|
|
1045
|
+
//# sourceMappingURL=chunk-EJXGN76H.mjs.map
|