@signaltree/guardrails 4.1.1 → 4.1.2
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/LICENSE +54 -0
- package/package.json +14 -14
- package/dist/factories/index.js +0 -119
- package/dist/lib/guardrails.js +0 -612
- package/dist/lib/rules.js +0 -62
- package/dist/noop.js +0 -21
- package/src/factories/index.d.ts +0 -20
- package/src/index.d.ts +0 -4
- package/src/lib/guardrails.d.ts +0 -3
- package/src/lib/rules.d.ts +0 -8
- package/src/lib/types.d.ts +0 -138
- package/src/noop.d.ts +0 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
BUSINESS SOURCE LICENSE 1.1
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jonathan D Borgia
|
|
4
|
+
|
|
5
|
+
This Business Source License 1.1 ("License") governs the use of the software and associated documentation files (the "Software"). You are granted a limited license to use the Software under the terms of this License.
|
|
6
|
+
|
|
7
|
+
1. Definitions
|
|
8
|
+
|
|
9
|
+
"Change Date" means the date on which the Change License set out in section 6 will apply to the Software. The Change Date for this release is 2028-09-05.
|
|
10
|
+
|
|
11
|
+
"Change License" means the open source license that will apply to the Software on and after the Change Date. The Change License for this release is the MIT License.
|
|
12
|
+
|
|
13
|
+
"Licensor" means the copyright owner granting rights under this License (Jonathan D Borgia).
|
|
14
|
+
|
|
15
|
+
"You" ("Licensee") means an individual or legal entity exercising rights under this License who has not violated the terms of this License or had their rights terminated.
|
|
16
|
+
|
|
17
|
+
2. License Grant
|
|
18
|
+
|
|
19
|
+
Subject to the terms and conditions of this License, Licensor hereby grants You a non-exclusive, non-transferable, worldwide license to use, reproduce, display, perform, and distribute the Software, and to make modifications and derivative works for internal use, until the Change Date.
|
|
20
|
+
|
|
21
|
+
3. Commercial Use
|
|
22
|
+
|
|
23
|
+
You may use the Software in commercial applications, including for providing services, selling products that include the Software, or otherwise exploiting the Software commercially, subject to the other terms of this License.
|
|
24
|
+
|
|
25
|
+
4. Limitations and Conditions
|
|
26
|
+
|
|
27
|
+
a. You may not remove or alter this License, the copyright notice, or notices of the Change Date.
|
|
28
|
+
|
|
29
|
+
b. You may not publicly offer a modified version of the Software that would directly compete with Licensor's public offering of the Software if doing so would circumvent the intent of this License.
|
|
30
|
+
|
|
31
|
+
c. Except as expressly provided in this License, no rights are granted to You under any patent or trademark of Licensor.
|
|
32
|
+
|
|
33
|
+
5. Disclaimer and Limitation of Liability
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. TO THE FULLEST EXTENT PERMITTED BY LAW, LICENSOR WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM OR RELATING TO THE SOFTWARE.
|
|
36
|
+
|
|
37
|
+
6. Change License
|
|
38
|
+
|
|
39
|
+
On and after the Change Date specified above, the Software will be licensed under the Change License (MIT License) on the same terms and conditions as set forth by that Change License.
|
|
40
|
+
|
|
41
|
+
7. Governing Law
|
|
42
|
+
|
|
43
|
+
This License will be governed by and construed in accordance with the laws of the State of New York, USA, without regard to conflict of law principles.
|
|
44
|
+
|
|
45
|
+
8. Accepting this License
|
|
46
|
+
|
|
47
|
+
You accept this License by copying, modifying, or distributing the Software or any portion thereof.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
LICENSE NOTE
|
|
52
|
+
|
|
53
|
+
- Original license file replaced on 2025-09-05 to Business Source License 1.1. Change Date: 2028-09-05. Change License: MIT.
|
|
54
|
+
or standard modifications for your own applications.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/guardrails",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "Development-only performance monitoring and anti-pattern detection for SignalTree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -48,22 +48,14 @@
|
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"build": "nx build guardrails",
|
|
53
|
-
"test": "nx test guardrails",
|
|
54
|
-
"lint": "nx lint guardrails",
|
|
55
|
-
"test:watch": "nx test guardrails --watch",
|
|
56
|
-
"test:coverage": "nx test guardrails --coverage",
|
|
57
|
-
"type-check": "tsc --project tsconfig.lib.json --noEmit"
|
|
58
|
-
},
|
|
59
51
|
"peerDependencies": {
|
|
60
|
-
"@signaltree/core": "4.1.
|
|
52
|
+
"@signaltree/core": "4.1.2",
|
|
61
53
|
"tslib": "^2.0.0"
|
|
62
54
|
},
|
|
63
55
|
"devDependencies": {
|
|
64
|
-
"@signaltree/core": "
|
|
65
|
-
"@signaltree/shared": "
|
|
66
|
-
"@signaltree/types": "
|
|
56
|
+
"@signaltree/core": "4.1.0",
|
|
57
|
+
"@signaltree/shared": "0.0.1",
|
|
58
|
+
"@signaltree/types": "4.1.2"
|
|
67
59
|
},
|
|
68
60
|
"keywords": [
|
|
69
61
|
"signaltree",
|
|
@@ -81,5 +73,13 @@
|
|
|
81
73
|
"type": "git",
|
|
82
74
|
"url": "https://github.com/signaltree/signaltree.git",
|
|
83
75
|
"directory": "packages/guardrails"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "nx build guardrails",
|
|
79
|
+
"test": "nx test guardrails",
|
|
80
|
+
"lint": "nx lint guardrails",
|
|
81
|
+
"test:watch": "nx test guardrails --watch",
|
|
82
|
+
"test:coverage": "nx test guardrails --coverage",
|
|
83
|
+
"type-check": "tsc --project tsconfig.lib.json --noEmit"
|
|
84
84
|
}
|
|
85
|
-
}
|
|
85
|
+
}
|
package/dist/factories/index.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { withGuardrails } from '../lib/guardrails.js';
|
|
2
|
-
import { rules } from '../lib/rules.js';
|
|
3
|
-
|
|
4
|
-
function isGuardrailsConfig(value) {
|
|
5
|
-
return Boolean(value) && typeof value === 'object';
|
|
6
|
-
}
|
|
7
|
-
function resolveGuardrailsConfig(guardrails) {
|
|
8
|
-
if (guardrails === false) {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
if (isGuardrailsConfig(guardrails)) {
|
|
12
|
-
return guardrails;
|
|
13
|
-
}
|
|
14
|
-
return {
|
|
15
|
-
budgets: {
|
|
16
|
-
maxUpdateTime: 16,
|
|
17
|
-
maxRecomputations: 100
|
|
18
|
-
},
|
|
19
|
-
hotPaths: {
|
|
20
|
-
enabled: true,
|
|
21
|
-
threshold: 10
|
|
22
|
-
},
|
|
23
|
-
reporting: {
|
|
24
|
-
console: true
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function createFeatureTree(signalTree, initial, options) {
|
|
29
|
-
const env = options.env ?? process?.env?.['NODE_ENV'] ?? 'production';
|
|
30
|
-
const isDev = env === 'development';
|
|
31
|
-
const isTest = env === 'test';
|
|
32
|
-
const enhancers = [];
|
|
33
|
-
if (isDev || isTest) {
|
|
34
|
-
const guardrailsConfig = resolveGuardrailsConfig(options.guardrails);
|
|
35
|
-
if (guardrailsConfig) {
|
|
36
|
-
enhancers.push(withGuardrails(guardrailsConfig));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (options.enhancers?.length) {
|
|
40
|
-
enhancers.push(...options.enhancers);
|
|
41
|
-
}
|
|
42
|
-
let tree = signalTree(initial);
|
|
43
|
-
for (const enhancer of enhancers) {
|
|
44
|
-
tree = tree.with(enhancer);
|
|
45
|
-
}
|
|
46
|
-
return tree;
|
|
47
|
-
}
|
|
48
|
-
function createAngularFeatureTree(signalTree, initial, options) {
|
|
49
|
-
const isDev = Boolean(ngDevMode);
|
|
50
|
-
return createFeatureTree(signalTree, initial, {
|
|
51
|
-
...options,
|
|
52
|
-
env: isDev ? 'development' : 'production'
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function createAppShellTree(signalTree, initial) {
|
|
56
|
-
return createFeatureTree(signalTree, initial, {
|
|
57
|
-
guardrails: {
|
|
58
|
-
budgets: {
|
|
59
|
-
maxUpdateTime: 4,
|
|
60
|
-
maxMemory: 20
|
|
61
|
-
},
|
|
62
|
-
hotPaths: {
|
|
63
|
-
threshold: 5
|
|
64
|
-
},
|
|
65
|
-
customRules: [rules.noDeepNesting(3)]
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function createPerformanceTree(signalTree, initial, name) {
|
|
70
|
-
return createFeatureTree(signalTree, initial, {
|
|
71
|
-
guardrails: {
|
|
72
|
-
budgets: {
|
|
73
|
-
maxUpdateTime: 8,
|
|
74
|
-
maxRecomputations: 200
|
|
75
|
-
},
|
|
76
|
-
hotPaths: {
|
|
77
|
-
threshold: 50
|
|
78
|
-
},
|
|
79
|
-
memoryLeaks: {
|
|
80
|
-
enabled: false
|
|
81
|
-
},
|
|
82
|
-
reporting: {
|
|
83
|
-
interval: 10000
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
function createFormTree(signalTree, initial, formName) {
|
|
89
|
-
return createFeatureTree(signalTree, initial, {
|
|
90
|
-
guardrails: {
|
|
91
|
-
customRules: [rules.noDeepNesting(4), rules.maxPayloadSize(50), rules.noSensitiveData()]
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function createCacheTree(signalTree, initial) {
|
|
96
|
-
return createFeatureTree(signalTree, initial, {
|
|
97
|
-
guardrails: {
|
|
98
|
-
mode: 'silent',
|
|
99
|
-
memoryLeaks: {
|
|
100
|
-
enabled: false
|
|
101
|
-
}
|
|
102
|
-
}});
|
|
103
|
-
}
|
|
104
|
-
function createTestTree(signalTree, initial, overrides) {
|
|
105
|
-
return createFeatureTree(signalTree, initial, {
|
|
106
|
-
env: 'test',
|
|
107
|
-
guardrails: {
|
|
108
|
-
mode: 'throw',
|
|
109
|
-
budgets: {
|
|
110
|
-
maxUpdateTime: 5,
|
|
111
|
-
maxRecomputations: 50
|
|
112
|
-
},
|
|
113
|
-
customRules: [rules.noFunctionsInState(), rules.noDeepNesting(4)],
|
|
114
|
-
...overrides
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export { createAngularFeatureTree, createAppShellTree, createCacheTree, createFeatureTree, createFormTree, createPerformanceTree, createTestTree };
|
package/dist/lib/guardrails.js
DELETED
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
function isFunction(value) {
|
|
2
|
-
return typeof value === 'function';
|
|
3
|
-
}
|
|
4
|
-
function isString(value) {
|
|
5
|
-
return typeof value === 'string';
|
|
6
|
-
}
|
|
7
|
-
function isObjectLike(value) {
|
|
8
|
-
return typeof value === 'object' && value !== null;
|
|
9
|
-
}
|
|
10
|
-
function resolveEnabledFlag(option) {
|
|
11
|
-
if (option === undefined) {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
if (isFunction(option)) {
|
|
15
|
-
try {
|
|
16
|
-
return option();
|
|
17
|
-
} catch {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return option;
|
|
22
|
-
}
|
|
23
|
-
function supportsMiddleware(tree) {
|
|
24
|
-
const candidate = tree;
|
|
25
|
-
return isFunction(candidate.addTap) && isFunction(candidate.removeTap);
|
|
26
|
-
}
|
|
27
|
-
function tryStructuredClone(value) {
|
|
28
|
-
const cloneFn = globalThis.structuredClone;
|
|
29
|
-
if (isFunction(cloneFn)) {
|
|
30
|
-
try {
|
|
31
|
-
return cloneFn(value);
|
|
32
|
-
} catch {}
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
function isDevEnvironment() {
|
|
37
|
-
if (__DEV__ !== undefined) return __DEV__;
|
|
38
|
-
if (process?.env?.['NODE_ENV'] === 'production') return false;
|
|
39
|
-
if (ngDevMode != null) return Boolean(ngDevMode);
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
const MAX_TIMING_SAMPLES = 1000;
|
|
43
|
-
const RECOMPUTATION_WINDOW_MS = 1000;
|
|
44
|
-
function withGuardrails(config = {}) {
|
|
45
|
-
return tree => {
|
|
46
|
-
const enabled = resolveEnabledFlag(config.enabled);
|
|
47
|
-
if (!isDevEnvironment() || !enabled) {
|
|
48
|
-
return tree;
|
|
49
|
-
}
|
|
50
|
-
if (!supportsMiddleware(tree)) {
|
|
51
|
-
console.warn('[Guardrails] Tree does not expose middleware hooks; guardrails disabled.');
|
|
52
|
-
return tree;
|
|
53
|
-
}
|
|
54
|
-
const stats = createRuntimeStats();
|
|
55
|
-
const context = {
|
|
56
|
-
tree,
|
|
57
|
-
config,
|
|
58
|
-
stats,
|
|
59
|
-
issues: [],
|
|
60
|
-
hotPaths: [],
|
|
61
|
-
currentUpdate: null,
|
|
62
|
-
suppressed: false,
|
|
63
|
-
timings: [],
|
|
64
|
-
hotPathData: new Map(),
|
|
65
|
-
issueMap: new Map(),
|
|
66
|
-
signalUsage: new Map(),
|
|
67
|
-
memoryHistory: [],
|
|
68
|
-
recomputationLog: [],
|
|
69
|
-
disposed: false
|
|
70
|
-
};
|
|
71
|
-
const middlewareId = `guardrails:${config.treeId ?? 'tree'}:${Math.random().toString(36).slice(2)}`;
|
|
72
|
-
context.middlewareId = middlewareId;
|
|
73
|
-
const middleware = createGuardrailsMiddleware(context);
|
|
74
|
-
tree.addTap(middleware);
|
|
75
|
-
const stopMonitoring = startMonitoring(context);
|
|
76
|
-
const teardown = () => {
|
|
77
|
-
if (context.disposed) return;
|
|
78
|
-
context.disposed = true;
|
|
79
|
-
stopMonitoring();
|
|
80
|
-
try {
|
|
81
|
-
tree.removeTap(middlewareId);
|
|
82
|
-
} catch {}
|
|
83
|
-
};
|
|
84
|
-
const originalDestroy = tree.destroy?.bind(tree);
|
|
85
|
-
tree.destroy = () => {
|
|
86
|
-
teardown();
|
|
87
|
-
if (originalDestroy) {
|
|
88
|
-
originalDestroy();
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
tree['__guardrails'] = createAPI(context, teardown);
|
|
92
|
-
return tree;
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function createRuntimeStats() {
|
|
96
|
-
return {
|
|
97
|
-
updateCount: 0,
|
|
98
|
-
totalUpdateTime: 0,
|
|
99
|
-
avgUpdateTime: 0,
|
|
100
|
-
p50UpdateTime: 0,
|
|
101
|
-
p95UpdateTime: 0,
|
|
102
|
-
p99UpdateTime: 0,
|
|
103
|
-
maxUpdateTime: 0,
|
|
104
|
-
recomputationCount: 0,
|
|
105
|
-
recomputationsPerSecond: 0,
|
|
106
|
-
signalCount: 0,
|
|
107
|
-
signalRetention: 0,
|
|
108
|
-
unreadSignalCount: 0,
|
|
109
|
-
memoryGrowthRate: 0,
|
|
110
|
-
hotPathCount: 0,
|
|
111
|
-
violationCount: 0
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function createGuardrailsMiddleware(context) {
|
|
115
|
-
return {
|
|
116
|
-
id: context.middlewareId ?? 'guardrails',
|
|
117
|
-
before: (action, payload, state) => {
|
|
118
|
-
if (context.suppressed) {
|
|
119
|
-
context.currentUpdate = null;
|
|
120
|
-
return !context.disposed;
|
|
121
|
-
}
|
|
122
|
-
const metadata = extractMetadata(payload);
|
|
123
|
-
if (shouldSuppressUpdate(context, metadata)) {
|
|
124
|
-
context.currentUpdate = null;
|
|
125
|
-
return !context.disposed;
|
|
126
|
-
}
|
|
127
|
-
const details = collectUpdateDetails(payload, state);
|
|
128
|
-
context.currentUpdate = {
|
|
129
|
-
action,
|
|
130
|
-
startTime: performance.now(),
|
|
131
|
-
metadata,
|
|
132
|
-
details
|
|
133
|
-
};
|
|
134
|
-
for (const detail of details) {
|
|
135
|
-
analyzePreUpdate(context, detail, metadata);
|
|
136
|
-
}
|
|
137
|
-
return !context.disposed;
|
|
138
|
-
},
|
|
139
|
-
after: (_action, _payload, _previousState, newState) => {
|
|
140
|
-
const pending = context.currentUpdate;
|
|
141
|
-
if (!pending) return;
|
|
142
|
-
const duration = Math.max(0, performance.now() - pending.startTime);
|
|
143
|
-
const timestamp = Date.now();
|
|
144
|
-
const recomputations = Math.max(0, pending.details.length - 1);
|
|
145
|
-
updateTimingStats(context, duration);
|
|
146
|
-
for (const [index, detail] of pending.details.entries()) {
|
|
147
|
-
const latest = getValueAtPath(newState, detail.segments);
|
|
148
|
-
const diffRatio = calculateDiffRatio(detail.oldValue, latest);
|
|
149
|
-
analyzePostUpdate(context, detail, duration, diffRatio, index === 0);
|
|
150
|
-
trackHotPath(context, detail.path, duration);
|
|
151
|
-
trackSignalUsage(context, detail.path, timestamp);
|
|
152
|
-
}
|
|
153
|
-
updateSignalStats(context, timestamp);
|
|
154
|
-
recordRecomputations(context, recomputations, timestamp);
|
|
155
|
-
context.currentUpdate = null;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function updatePercentiles(context) {
|
|
160
|
-
if (context.timings.length === 0) return;
|
|
161
|
-
const sorted = [...context.timings].sort((a, b) => a - b);
|
|
162
|
-
context.stats.p50UpdateTime = sorted[Math.floor(sorted.length * 0.5)] || 0;
|
|
163
|
-
context.stats.p95UpdateTime = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
|
164
|
-
context.stats.p99UpdateTime = sorted[Math.floor(sorted.length * 0.99)] || 0;
|
|
165
|
-
}
|
|
166
|
-
function calculateDiffRatio(oldValue, newValue) {
|
|
167
|
-
if (!isComparableRecord(oldValue) || !isComparableRecord(newValue)) {
|
|
168
|
-
return Object.is(oldValue, newValue) ? 0 : 1;
|
|
169
|
-
}
|
|
170
|
-
if (oldValue === newValue) return 0;
|
|
171
|
-
const oldKeys = new Set(Object.keys(oldValue));
|
|
172
|
-
const newKeys = new Set(Object.keys(newValue));
|
|
173
|
-
const allKeys = new Set([...oldKeys, ...newKeys]);
|
|
174
|
-
let changed = 0;
|
|
175
|
-
for (const key of allKeys) {
|
|
176
|
-
if (!oldKeys.has(key) || !newKeys.has(key) || oldValue[key] !== newValue[key]) {
|
|
177
|
-
changed++;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return allKeys.size === 0 ? 0 : changed / allKeys.size;
|
|
181
|
-
}
|
|
182
|
-
function analyzePreUpdate(context, detail, metadata) {
|
|
183
|
-
if (!context.config.customRules) return;
|
|
184
|
-
for (const rule of context.config.customRules) {
|
|
185
|
-
evaluateRule(context, rule, {
|
|
186
|
-
path: detail.segments,
|
|
187
|
-
value: detail.newValue,
|
|
188
|
-
oldValue: detail.oldValue,
|
|
189
|
-
metadata,
|
|
190
|
-
tree: context.tree,
|
|
191
|
-
stats: context.stats
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
function analyzePostUpdate(context, detail, duration, diffRatio, isPrimary) {
|
|
196
|
-
if (isPrimary && context.config.budgets?.maxUpdateTime && duration > context.config.budgets.maxUpdateTime) {
|
|
197
|
-
addIssue(context, {
|
|
198
|
-
type: 'budget',
|
|
199
|
-
severity: 'error',
|
|
200
|
-
message: `Update took ${duration.toFixed(2)}ms (budget: ${context.config.budgets.maxUpdateTime}ms)`,
|
|
201
|
-
path: detail.path,
|
|
202
|
-
count: 1
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
const minDiff = context.config.analysis?.minDiffForParentReplace ?? 0.8;
|
|
206
|
-
if (context.config.analysis?.warnParentReplace && diffRatio > minDiff) {
|
|
207
|
-
addIssue(context, {
|
|
208
|
-
type: 'analysis',
|
|
209
|
-
severity: 'warning',
|
|
210
|
-
message: `High diff ratio (${(diffRatio * 100).toFixed(0)}%) - consider scoped updates`,
|
|
211
|
-
path: detail.path,
|
|
212
|
-
count: 1,
|
|
213
|
-
diffRatio
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function trackHotPath(context, path, duration) {
|
|
218
|
-
if (!context.config.hotPaths?.enabled) return;
|
|
219
|
-
const pathKey = Array.isArray(path) ? path.join('.') : path;
|
|
220
|
-
const now = Date.now();
|
|
221
|
-
const windowMs = context.config.hotPaths.windowMs || 1000;
|
|
222
|
-
let data = context.hotPathData.get(pathKey);
|
|
223
|
-
if (!data) {
|
|
224
|
-
data = {
|
|
225
|
-
count: 0,
|
|
226
|
-
lastUpdate: now,
|
|
227
|
-
durations: []
|
|
228
|
-
};
|
|
229
|
-
context.hotPathData.set(pathKey, data);
|
|
230
|
-
}
|
|
231
|
-
if (now - data.lastUpdate > windowMs) {
|
|
232
|
-
data.count = 0;
|
|
233
|
-
data.durations = [];
|
|
234
|
-
}
|
|
235
|
-
data.count++;
|
|
236
|
-
data.durations.push(duration);
|
|
237
|
-
data.lastUpdate = now;
|
|
238
|
-
const threshold = context.config.hotPaths.threshold || 10;
|
|
239
|
-
const updatesPerSecond = data.count / windowMs * 1000;
|
|
240
|
-
if (updatesPerSecond > threshold) {
|
|
241
|
-
const sorted = [...data.durations].sort((a, b) => a - b);
|
|
242
|
-
const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
|
243
|
-
const avg = data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length;
|
|
244
|
-
updateHotPath(context, {
|
|
245
|
-
path: pathKey,
|
|
246
|
-
updatesPerSecond,
|
|
247
|
-
heatScore: Math.min(100, updatesPerSecond / threshold * 50),
|
|
248
|
-
downstreamEffects: 0,
|
|
249
|
-
avgDuration: avg,
|
|
250
|
-
p95Duration: p95
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function trackSignalUsage(context, path, timestamp) {
|
|
255
|
-
const key = Array.isArray(path) ? path.join('.') : path;
|
|
256
|
-
const entry = context.signalUsage.get(key) ?? {
|
|
257
|
-
updates: 0,
|
|
258
|
-
lastSeen: timestamp
|
|
259
|
-
};
|
|
260
|
-
entry.updates += 1;
|
|
261
|
-
entry.lastSeen = timestamp;
|
|
262
|
-
context.signalUsage.set(key, entry);
|
|
263
|
-
}
|
|
264
|
-
function updateSignalStats(context, timestamp) {
|
|
265
|
-
const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5000;
|
|
266
|
-
const historyWindow = Math.max(retentionWindow, 1000);
|
|
267
|
-
const signalCount = context.signalUsage.size;
|
|
268
|
-
context.stats.signalCount = signalCount;
|
|
269
|
-
const staleCount = [...context.signalUsage.values()].filter(entry => timestamp - entry.lastSeen > retentionWindow).length;
|
|
270
|
-
context.stats.signalRetention = staleCount;
|
|
271
|
-
context.stats.unreadSignalCount = 0;
|
|
272
|
-
context.memoryHistory.push({
|
|
273
|
-
timestamp,
|
|
274
|
-
count: signalCount
|
|
275
|
-
});
|
|
276
|
-
context.memoryHistory = context.memoryHistory.filter(entry => timestamp - entry.timestamp <= historyWindow);
|
|
277
|
-
const baseline = context.memoryHistory[0]?.count ?? signalCount;
|
|
278
|
-
const growth = baseline === 0 ? 0 : (signalCount - baseline) / Math.max(1, baseline);
|
|
279
|
-
context.stats.memoryGrowthRate = growth;
|
|
280
|
-
}
|
|
281
|
-
function recordRecomputations(context, count, timestamp) {
|
|
282
|
-
if (count > 0) {
|
|
283
|
-
context.stats.recomputationCount += count;
|
|
284
|
-
for (let i = 0; i < count; i++) {
|
|
285
|
-
context.recomputationLog.push(timestamp);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (context.recomputationLog.length) {
|
|
289
|
-
context.recomputationLog = context.recomputationLog.filter(value => timestamp - value <= RECOMPUTATION_WINDOW_MS);
|
|
290
|
-
}
|
|
291
|
-
context.stats.recomputationsPerSecond = context.recomputationLog.length;
|
|
292
|
-
}
|
|
293
|
-
function updateHotPath(context, hotPath) {
|
|
294
|
-
const existing = context.hotPaths.find(h => h.path === hotPath.path);
|
|
295
|
-
if (existing) {
|
|
296
|
-
Object.assign(existing, hotPath);
|
|
297
|
-
} else {
|
|
298
|
-
context.hotPaths.push(hotPath);
|
|
299
|
-
const topN = context.config.hotPaths?.topN || 5;
|
|
300
|
-
if (context.hotPaths.length > topN) {
|
|
301
|
-
context.hotPaths.sort((a, b) => b.heatScore - a.heatScore);
|
|
302
|
-
context.hotPaths.length = topN;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
context.stats.hotPathCount = context.hotPaths.length;
|
|
306
|
-
}
|
|
307
|
-
function evaluateRule(context, rule, ruleContext) {
|
|
308
|
-
const handleFailure = () => {
|
|
309
|
-
const message = typeof rule.message === 'function' ? rule.message(ruleContext) : rule.message;
|
|
310
|
-
addIssue(context, {
|
|
311
|
-
type: 'rule',
|
|
312
|
-
severity: rule.severity || 'warning',
|
|
313
|
-
message,
|
|
314
|
-
path: ruleContext.path.join('.'),
|
|
315
|
-
count: 1,
|
|
316
|
-
metadata: {
|
|
317
|
-
rule: rule.name
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
try {
|
|
322
|
-
const result = rule.test(ruleContext);
|
|
323
|
-
if (result instanceof Promise) {
|
|
324
|
-
result.then(outcome => {
|
|
325
|
-
if (!outcome) {
|
|
326
|
-
handleFailure();
|
|
327
|
-
}
|
|
328
|
-
}).catch(error => {
|
|
329
|
-
console.warn(`[Guardrails] Rule ${rule.name} rejected:`, error);
|
|
330
|
-
});
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (!result) {
|
|
334
|
-
handleFailure();
|
|
335
|
-
}
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.warn(`[Guardrails] Rule ${rule.name} threw error:`, error);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function addIssue(context, issue) {
|
|
341
|
-
if (context.suppressed) return;
|
|
342
|
-
if (context.config.reporting?.aggregateWarnings !== false) {
|
|
343
|
-
const key = `${issue.type}:${issue.path}:${issue.message}`;
|
|
344
|
-
const existing = context.issueMap.get(key);
|
|
345
|
-
if (existing) {
|
|
346
|
-
existing.count++;
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
context.issueMap.set(key, issue);
|
|
350
|
-
}
|
|
351
|
-
context.issues.push(issue);
|
|
352
|
-
context.stats.violationCount++;
|
|
353
|
-
if (context.config.mode === 'throw') {
|
|
354
|
-
throw new Error(`[Guardrails] ${issue.message}`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
function shouldSuppressUpdate(context, metadata) {
|
|
358
|
-
if (context.suppressed) return true;
|
|
359
|
-
if (!metadata) return false;
|
|
360
|
-
if (metadata.suppressGuardrails && context.config.suppression?.respectMetadata !== false) {
|
|
361
|
-
return true;
|
|
362
|
-
}
|
|
363
|
-
const autoSuppress = new Set(context.config.suppression?.autoSuppress ?? []);
|
|
364
|
-
return [metadata.intent, metadata.source].some(value => isString(value) && autoSuppress.has(value));
|
|
365
|
-
}
|
|
366
|
-
function startMonitoring(context) {
|
|
367
|
-
const interval = setInterval(() => {
|
|
368
|
-
if (context.disposed) {
|
|
369
|
-
clearInterval(interval);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
checkMemory(context);
|
|
373
|
-
maybeReport(context);
|
|
374
|
-
}, context.config.reporting?.interval || 5000);
|
|
375
|
-
return () => clearInterval(interval);
|
|
376
|
-
}
|
|
377
|
-
function checkMemory(context) {
|
|
378
|
-
if (!context.config.memoryLeaks?.enabled) return;
|
|
379
|
-
const now = Date.now();
|
|
380
|
-
const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5000;
|
|
381
|
-
const retentionThreshold = context.config.memoryLeaks?.retentionThreshold ?? 100;
|
|
382
|
-
const growthThreshold = context.config.memoryLeaks?.growthRate ?? 0.2;
|
|
383
|
-
const staleCount = [...context.signalUsage.values()].filter(entry => now - entry.lastSeen > retentionWindow).length;
|
|
384
|
-
context.stats.signalRetention = staleCount;
|
|
385
|
-
const exceedsRetention = context.stats.signalRetention > retentionThreshold;
|
|
386
|
-
const exceedsGrowth = context.stats.memoryGrowthRate > growthThreshold;
|
|
387
|
-
if (exceedsRetention || exceedsGrowth) {
|
|
388
|
-
addIssue(context, {
|
|
389
|
-
type: 'memory',
|
|
390
|
-
severity: 'warning',
|
|
391
|
-
message: `Potential memory leak detected (signals: ${context.stats.signalCount}, growth ${(context.stats.memoryGrowthRate * 100).toFixed(1)}%)`,
|
|
392
|
-
path: 'root',
|
|
393
|
-
count: 1,
|
|
394
|
-
metadata: {
|
|
395
|
-
signalCount: context.stats.signalCount,
|
|
396
|
-
growth: context.stats.memoryGrowthRate
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function maybeReport(context) {
|
|
402
|
-
if (context.config.reporting?.console === false) return;
|
|
403
|
-
const report = generateReport(context);
|
|
404
|
-
if (context.config.reporting?.customReporter) {
|
|
405
|
-
context.config.reporting.customReporter(report);
|
|
406
|
-
}
|
|
407
|
-
if (context.config.reporting?.console && context.issues.length > 0) {
|
|
408
|
-
reportToConsole(report, context.config.reporting.console === 'verbose');
|
|
409
|
-
}
|
|
410
|
-
context.issues = [];
|
|
411
|
-
context.issueMap.clear();
|
|
412
|
-
}
|
|
413
|
-
function reportToConsole(report, verbose) {
|
|
414
|
-
console.group('[Guardrails] Performance Report');
|
|
415
|
-
logIssues(report.issues);
|
|
416
|
-
logHotPaths(report.hotPaths);
|
|
417
|
-
if (verbose) {
|
|
418
|
-
logVerboseStats(report);
|
|
419
|
-
}
|
|
420
|
-
console.groupEnd();
|
|
421
|
-
}
|
|
422
|
-
function logIssues(issues) {
|
|
423
|
-
if (issues.length === 0) return;
|
|
424
|
-
console.warn(`${issues.length} issues detected:`);
|
|
425
|
-
for (const issue of issues) {
|
|
426
|
-
const prefix = getSeverityPrefix(issue.severity);
|
|
427
|
-
const countSuffix = issue.count > 1 ? ` (x${issue.count})` : '';
|
|
428
|
-
const message = `${prefix} [${issue.type}] ${issue.message}${countSuffix}`;
|
|
429
|
-
console.log(` ${message}`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function logHotPaths(hotPaths) {
|
|
433
|
-
if (hotPaths.length === 0) return;
|
|
434
|
-
console.log(`\nHot Paths (${hotPaths.length}):`);
|
|
435
|
-
for (const hp of hotPaths) {
|
|
436
|
-
const entry = ` 🔥 ${hp.path}: ${hp.updatesPerSecond.toFixed(1)}/s (heat: ${hp.heatScore.toFixed(0)})`;
|
|
437
|
-
console.log(entry);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
function logVerboseStats(report) {
|
|
441
|
-
console.log('\nStats:', report.stats);
|
|
442
|
-
console.log('Budgets:', report.budgets);
|
|
443
|
-
}
|
|
444
|
-
function getSeverityPrefix(severity) {
|
|
445
|
-
if (severity === 'error') return '❌';
|
|
446
|
-
if (severity === 'warning') return '⚠️';
|
|
447
|
-
return 'ℹ️';
|
|
448
|
-
}
|
|
449
|
-
function generateReport(context) {
|
|
450
|
-
const memoryCurrent = context.stats.signalCount;
|
|
451
|
-
const memoryLimit = context.config.budgets?.maxMemory ?? 50;
|
|
452
|
-
const recomputationCurrent = context.stats.recomputationsPerSecond;
|
|
453
|
-
const recomputationLimit = context.config.budgets?.maxRecomputations ?? 100;
|
|
454
|
-
const budgets = {
|
|
455
|
-
updateTime: createBudgetItem(context.stats.avgUpdateTime, context.config.budgets?.maxUpdateTime || 16),
|
|
456
|
-
memory: createBudgetItem(memoryCurrent, memoryLimit),
|
|
457
|
-
recomputations: createBudgetItem(recomputationCurrent, recomputationLimit)
|
|
458
|
-
};
|
|
459
|
-
return {
|
|
460
|
-
timestamp: Date.now(),
|
|
461
|
-
treeId: context.config.treeId,
|
|
462
|
-
issues: Array.from(context.issueMap.values()),
|
|
463
|
-
hotPaths: context.hotPaths,
|
|
464
|
-
budgets,
|
|
465
|
-
stats: context.stats,
|
|
466
|
-
recommendations: generateRecommendations(context)
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function createBudgetItem(current, limit) {
|
|
470
|
-
if (limit <= 0) {
|
|
471
|
-
return {
|
|
472
|
-
current,
|
|
473
|
-
limit,
|
|
474
|
-
usage: 0,
|
|
475
|
-
status: 'ok'
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
const usage = current / limit * 100;
|
|
479
|
-
const threshold = 80;
|
|
480
|
-
let status;
|
|
481
|
-
if (usage > 100) {
|
|
482
|
-
status = 'exceeded';
|
|
483
|
-
} else if (usage > threshold) {
|
|
484
|
-
status = 'warning';
|
|
485
|
-
} else {
|
|
486
|
-
status = 'ok';
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
current,
|
|
490
|
-
limit,
|
|
491
|
-
usage,
|
|
492
|
-
status
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
function generateRecommendations(context) {
|
|
496
|
-
const recommendations = [];
|
|
497
|
-
if (context.hotPaths.length > 0) {
|
|
498
|
-
recommendations.push('Consider batching or debouncing updates to hot paths');
|
|
499
|
-
}
|
|
500
|
-
if (context.stats.avgUpdateTime > 10) {
|
|
501
|
-
recommendations.push('Average update time is high - review update logic');
|
|
502
|
-
}
|
|
503
|
-
return recommendations;
|
|
504
|
-
}
|
|
505
|
-
function createAPI(context, teardown) {
|
|
506
|
-
return {
|
|
507
|
-
getReport: () => generateReport(context),
|
|
508
|
-
getStats: () => context.stats,
|
|
509
|
-
suppress: fn => {
|
|
510
|
-
const was = context.suppressed;
|
|
511
|
-
context.suppressed = true;
|
|
512
|
-
try {
|
|
513
|
-
fn();
|
|
514
|
-
} finally {
|
|
515
|
-
context.suppressed = was;
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
dispose: () => {
|
|
519
|
-
teardown();
|
|
520
|
-
const finalReport = generateReport(context);
|
|
521
|
-
if (context.config.reporting?.console !== false) {
|
|
522
|
-
console.log('[Guardrails] Final report on disposal:', finalReport);
|
|
523
|
-
}
|
|
524
|
-
if (context.config.reporting?.customReporter) {
|
|
525
|
-
context.config.reporting.customReporter(finalReport);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
function extractMetadata(payload) {
|
|
531
|
-
if (!isObjectLike(payload)) return undefined;
|
|
532
|
-
const candidate = payload['metadata'];
|
|
533
|
-
return isObjectLike(candidate) ? candidate : undefined;
|
|
534
|
-
}
|
|
535
|
-
function collectUpdateDetails(payload, stateSnapshot) {
|
|
536
|
-
const details = [];
|
|
537
|
-
const visit = (value, segments, currentState) => {
|
|
538
|
-
const path = segments.length ? segments.join('.') : 'root';
|
|
539
|
-
const oldValue = captureValue(currentState);
|
|
540
|
-
if (isObjectLike(value)) {
|
|
541
|
-
details.push({
|
|
542
|
-
path,
|
|
543
|
-
segments: [...segments],
|
|
544
|
-
newValue: value,
|
|
545
|
-
oldValue
|
|
546
|
-
});
|
|
547
|
-
for (const [key, child] of Object.entries(value)) {
|
|
548
|
-
visit(child, [...segments, key], isObjectLike(currentState) ? currentState[key] : undefined);
|
|
549
|
-
}
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
details.push({
|
|
553
|
-
path,
|
|
554
|
-
segments: [...segments],
|
|
555
|
-
newValue: value,
|
|
556
|
-
oldValue
|
|
557
|
-
});
|
|
558
|
-
};
|
|
559
|
-
if (isObjectLike(payload)) {
|
|
560
|
-
visit(payload, [], stateSnapshot);
|
|
561
|
-
} else {
|
|
562
|
-
details.push({
|
|
563
|
-
path: 'root',
|
|
564
|
-
segments: [],
|
|
565
|
-
newValue: payload,
|
|
566
|
-
oldValue: captureValue(stateSnapshot)
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
if (details.length === 0) {
|
|
570
|
-
details.push({
|
|
571
|
-
path: 'root',
|
|
572
|
-
segments: [],
|
|
573
|
-
newValue: payload,
|
|
574
|
-
oldValue: captureValue(stateSnapshot)
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
return details;
|
|
578
|
-
}
|
|
579
|
-
function getValueAtPath(source, segments) {
|
|
580
|
-
let current = source;
|
|
581
|
-
for (const segment of segments) {
|
|
582
|
-
if (!isObjectLike(current)) {
|
|
583
|
-
return undefined;
|
|
584
|
-
}
|
|
585
|
-
current = current[segment];
|
|
586
|
-
}
|
|
587
|
-
return current;
|
|
588
|
-
}
|
|
589
|
-
function captureValue(value) {
|
|
590
|
-
return tryStructuredClone(value);
|
|
591
|
-
}
|
|
592
|
-
function isPlainObject(value) {
|
|
593
|
-
if (!value || typeof value !== 'object') return false;
|
|
594
|
-
const proto = Object.getPrototypeOf(value);
|
|
595
|
-
return proto === Object.prototype || proto === null;
|
|
596
|
-
}
|
|
597
|
-
function updateTimingStats(context, duration) {
|
|
598
|
-
context.timings.push(duration);
|
|
599
|
-
if (context.timings.length > MAX_TIMING_SAMPLES) {
|
|
600
|
-
context.timings.shift();
|
|
601
|
-
}
|
|
602
|
-
context.stats.updateCount++;
|
|
603
|
-
context.stats.totalUpdateTime += duration;
|
|
604
|
-
context.stats.avgUpdateTime = context.stats.totalUpdateTime / context.stats.updateCount;
|
|
605
|
-
context.stats.maxUpdateTime = Math.max(context.stats.maxUpdateTime, duration);
|
|
606
|
-
updatePercentiles(context);
|
|
607
|
-
}
|
|
608
|
-
function isComparableRecord(value) {
|
|
609
|
-
return isPlainObject(value);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
export { withGuardrails };
|
package/dist/lib/rules.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const rules = {
|
|
2
|
-
noDeepNesting: (maxDepth = 5) => ({
|
|
3
|
-
name: 'no-deep-nesting',
|
|
4
|
-
description: `Prevents nesting deeper than ${maxDepth} levels`,
|
|
5
|
-
test: ctx => ctx.path.length <= maxDepth,
|
|
6
|
-
message: ctx => `Path too deep: ${ctx.path.join('.')} (${ctx.path.length} levels, max: ${maxDepth})`,
|
|
7
|
-
severity: 'warning',
|
|
8
|
-
tags: ['architecture', 'complexity']
|
|
9
|
-
}),
|
|
10
|
-
noFunctionsInState: () => ({
|
|
11
|
-
name: 'no-functions',
|
|
12
|
-
description: 'Functions break serialization',
|
|
13
|
-
test: ctx => typeof ctx.value !== 'function',
|
|
14
|
-
message: 'Functions cannot be stored in state (breaks serialization)',
|
|
15
|
-
severity: 'error',
|
|
16
|
-
tags: ['serialization', 'data']
|
|
17
|
-
}),
|
|
18
|
-
noCacheInPersistence: () => ({
|
|
19
|
-
name: 'no-cache-persistence',
|
|
20
|
-
description: 'Prevent cache from being persisted',
|
|
21
|
-
test: ctx => {
|
|
22
|
-
if (ctx.metadata?.source === 'serialization' && ctx.path.includes('cache')) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
},
|
|
27
|
-
message: 'Cache should not be persisted',
|
|
28
|
-
severity: 'warning',
|
|
29
|
-
tags: ['persistence', 'cache']
|
|
30
|
-
}),
|
|
31
|
-
maxPayloadSize: (maxKB = 100) => ({
|
|
32
|
-
name: 'max-payload-size',
|
|
33
|
-
description: `Limit payload size to ${maxKB}KB`,
|
|
34
|
-
test: ctx => {
|
|
35
|
-
try {
|
|
36
|
-
const size = JSON.stringify(ctx.value).length;
|
|
37
|
-
return size < maxKB * 1024;
|
|
38
|
-
} catch {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
message: ctx => {
|
|
43
|
-
const size = JSON.stringify(ctx.value).length;
|
|
44
|
-
const kb = (size / 1024).toFixed(1);
|
|
45
|
-
return `Payload size ${kb}KB exceeds limit of ${maxKB}KB`;
|
|
46
|
-
},
|
|
47
|
-
severity: 'warning',
|
|
48
|
-
tags: ['performance', 'data']
|
|
49
|
-
}),
|
|
50
|
-
noSensitiveData: (sensitiveKeys = ['password', 'token', 'secret', 'apiKey']) => ({
|
|
51
|
-
name: 'no-sensitive-data',
|
|
52
|
-
description: 'Prevents storing sensitive data',
|
|
53
|
-
test: ctx => {
|
|
54
|
-
return !ctx.path.some(segment => sensitiveKeys.some(key => typeof segment === 'string' && segment.toLowerCase().includes(key.toLowerCase())));
|
|
55
|
-
},
|
|
56
|
-
message: ctx => `Sensitive data detected in path: ${ctx.path.join('.')}`,
|
|
57
|
-
severity: 'error',
|
|
58
|
-
tags: ['security', 'data']
|
|
59
|
-
})
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export { rules };
|
package/dist/noop.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const noopRule = name => ({
|
|
2
|
-
name,
|
|
3
|
-
description: 'No-op guardrail',
|
|
4
|
-
test: () => true,
|
|
5
|
-
message: '',
|
|
6
|
-
severity: 'info'
|
|
7
|
-
});
|
|
8
|
-
function withGuardrails(config) {
|
|
9
|
-
return tree => {
|
|
10
|
-
return tree;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
const rules = {
|
|
14
|
-
noDeepNesting: () => noopRule('noop'),
|
|
15
|
-
noFunctionsInState: () => noopRule('noop'),
|
|
16
|
-
noCacheInPersistence: () => noopRule('noop'),
|
|
17
|
-
maxPayloadSize: () => noopRule('noop'),
|
|
18
|
-
noSensitiveData: () => noopRule('noop')
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export { rules, withGuardrails };
|
package/src/factories/index.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { SignalTree, TreeConfig } from '@signaltree/core';
|
|
2
|
-
import type { GuardrailsConfig } from '../lib/types';
|
|
3
|
-
type SignalTreeFactory<T extends Record<string, unknown>> = (initial: T, config?: TreeConfig) => SignalTree<T>;
|
|
4
|
-
type EnhancerFn<T extends Record<string, unknown>> = (tree: SignalTree<T>) => SignalTree<T>;
|
|
5
|
-
interface FeatureTreeOptions<T extends Record<string, unknown>> {
|
|
6
|
-
name: string;
|
|
7
|
-
env?: 'development' | 'test' | 'staging' | 'production';
|
|
8
|
-
persistence?: boolean | Record<string, unknown>;
|
|
9
|
-
guardrails?: boolean | GuardrailsConfig;
|
|
10
|
-
devtools?: boolean;
|
|
11
|
-
enhancers?: EnhancerFn<T>[];
|
|
12
|
-
}
|
|
13
|
-
export declare function createFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: FeatureTreeOptions<T>): SignalTree<T>;
|
|
14
|
-
export declare function createAngularFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: Omit<FeatureTreeOptions<T>, 'env'>): SignalTree<T>;
|
|
15
|
-
export declare function createAppShellTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): SignalTree<T>;
|
|
16
|
-
export declare function createPerformanceTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, name: string): SignalTree<T>;
|
|
17
|
-
export declare function createFormTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, formName: string): SignalTree<T>;
|
|
18
|
-
export declare function createCacheTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): SignalTree<T>;
|
|
19
|
-
export declare function createTestTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, overrides?: Partial<GuardrailsConfig>): SignalTree<T>;
|
|
20
|
-
export {};
|
package/src/index.d.ts
DELETED
package/src/lib/guardrails.d.ts
DELETED
package/src/lib/rules.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { GuardrailRule } from './types';
|
|
2
|
-
export declare const rules: {
|
|
3
|
-
noDeepNesting: (maxDepth?: number) => GuardrailRule;
|
|
4
|
-
noFunctionsInState: () => GuardrailRule;
|
|
5
|
-
noCacheInPersistence: () => GuardrailRule;
|
|
6
|
-
maxPayloadSize: (maxKB?: number) => GuardrailRule;
|
|
7
|
-
noSensitiveData: (sensitiveKeys?: string[]) => GuardrailRule;
|
|
8
|
-
};
|
package/src/lib/types.d.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { SignalTree } from '@signaltree/core';
|
|
2
|
-
export interface GuardrailsConfig<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
3
|
-
mode?: 'warn' | 'throw' | 'silent';
|
|
4
|
-
enabled?: boolean | (() => boolean);
|
|
5
|
-
budgets?: {
|
|
6
|
-
maxUpdateTime?: number;
|
|
7
|
-
maxMemory?: number;
|
|
8
|
-
maxRecomputations?: number;
|
|
9
|
-
maxTreeDepth?: number;
|
|
10
|
-
alertThreshold?: number;
|
|
11
|
-
};
|
|
12
|
-
hotPaths?: {
|
|
13
|
-
enabled?: boolean;
|
|
14
|
-
threshold?: number;
|
|
15
|
-
topN?: number;
|
|
16
|
-
trackDownstream?: boolean;
|
|
17
|
-
windowMs?: number;
|
|
18
|
-
};
|
|
19
|
-
memoryLeaks?: {
|
|
20
|
-
enabled?: boolean;
|
|
21
|
-
checkInterval?: number;
|
|
22
|
-
retentionThreshold?: number;
|
|
23
|
-
growthRate?: number;
|
|
24
|
-
trackUnread?: boolean;
|
|
25
|
-
};
|
|
26
|
-
customRules?: GuardrailRule<T>[];
|
|
27
|
-
suppression?: {
|
|
28
|
-
autoSuppress?: Array<'hydrate' | 'reset' | 'bulk' | 'migration' | 'time-travel' | 'serialization'>;
|
|
29
|
-
respectMetadata?: boolean;
|
|
30
|
-
};
|
|
31
|
-
analysis?: {
|
|
32
|
-
forbidRootRead?: boolean;
|
|
33
|
-
forbidSliceRootRead?: boolean | string[];
|
|
34
|
-
maxDepsPerComputed?: number;
|
|
35
|
-
warnParentReplace?: boolean;
|
|
36
|
-
minDiffForParentReplace?: number;
|
|
37
|
-
detectThrashing?: boolean;
|
|
38
|
-
maxRerunsPerSecond?: number;
|
|
39
|
-
};
|
|
40
|
-
reporting?: {
|
|
41
|
-
interval?: number;
|
|
42
|
-
console?: boolean | 'verbose';
|
|
43
|
-
customReporter?: (report: GuardrailsReport) => void;
|
|
44
|
-
aggregateWarnings?: boolean;
|
|
45
|
-
maxIssuesPerReport?: number;
|
|
46
|
-
};
|
|
47
|
-
treeId?: string;
|
|
48
|
-
}
|
|
49
|
-
export interface UpdateMetadata {
|
|
50
|
-
intent?: 'hydrate' | 'reset' | 'bulk' | 'migration' | 'user' | 'system';
|
|
51
|
-
source?: 'serialization' | 'time-travel' | 'devtools' | 'user' | 'system';
|
|
52
|
-
suppressGuardrails?: boolean;
|
|
53
|
-
timestamp?: number;
|
|
54
|
-
correlationId?: string;
|
|
55
|
-
[key: string]: unknown;
|
|
56
|
-
}
|
|
57
|
-
export interface GuardrailRule<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
58
|
-
name: string;
|
|
59
|
-
description?: string;
|
|
60
|
-
test: (context: RuleContext<T>) => boolean | Promise<boolean>;
|
|
61
|
-
message: string | ((context: RuleContext<T>) => string);
|
|
62
|
-
severity?: 'error' | 'warning' | 'info';
|
|
63
|
-
fix?: (context: RuleContext<T>) => void;
|
|
64
|
-
tags?: string[];
|
|
65
|
-
}
|
|
66
|
-
export interface RuleContext<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
67
|
-
path: string[];
|
|
68
|
-
value: unknown;
|
|
69
|
-
oldValue?: unknown;
|
|
70
|
-
metadata?: UpdateMetadata;
|
|
71
|
-
tree: SignalTree<T>;
|
|
72
|
-
duration?: number;
|
|
73
|
-
diffRatio?: number;
|
|
74
|
-
recomputeCount?: number;
|
|
75
|
-
downstreamEffects?: number;
|
|
76
|
-
isUnread?: boolean;
|
|
77
|
-
stats: RuntimeStats;
|
|
78
|
-
}
|
|
79
|
-
export interface RuntimeStats {
|
|
80
|
-
updateCount: number;
|
|
81
|
-
totalUpdateTime: number;
|
|
82
|
-
avgUpdateTime: number;
|
|
83
|
-
p50UpdateTime: number;
|
|
84
|
-
p95UpdateTime: number;
|
|
85
|
-
p99UpdateTime: number;
|
|
86
|
-
maxUpdateTime: number;
|
|
87
|
-
recomputationCount: number;
|
|
88
|
-
recomputationsPerSecond: number;
|
|
89
|
-
signalCount: number;
|
|
90
|
-
signalRetention: number;
|
|
91
|
-
unreadSignalCount: number;
|
|
92
|
-
memoryGrowthRate: number;
|
|
93
|
-
hotPathCount: number;
|
|
94
|
-
violationCount: number;
|
|
95
|
-
}
|
|
96
|
-
export interface GuardrailIssue {
|
|
97
|
-
type: 'budget' | 'hot-path' | 'memory' | 'rule' | 'analysis';
|
|
98
|
-
severity: 'error' | 'warning' | 'info';
|
|
99
|
-
message: string;
|
|
100
|
-
path?: string;
|
|
101
|
-
count: number;
|
|
102
|
-
diffRatio?: number;
|
|
103
|
-
metadata?: Record<string, unknown>;
|
|
104
|
-
}
|
|
105
|
-
export interface HotPath {
|
|
106
|
-
path: string;
|
|
107
|
-
updatesPerSecond: number;
|
|
108
|
-
heatScore: number;
|
|
109
|
-
downstreamEffects: number;
|
|
110
|
-
avgDuration: number;
|
|
111
|
-
p95Duration: number;
|
|
112
|
-
}
|
|
113
|
-
export interface BudgetStatus {
|
|
114
|
-
updateTime: BudgetItem;
|
|
115
|
-
memory: BudgetItem;
|
|
116
|
-
recomputations: BudgetItem;
|
|
117
|
-
}
|
|
118
|
-
export interface BudgetItem {
|
|
119
|
-
current: number;
|
|
120
|
-
limit: number;
|
|
121
|
-
usage: number;
|
|
122
|
-
status: 'ok' | 'warning' | 'exceeded';
|
|
123
|
-
}
|
|
124
|
-
export interface GuardrailsReport {
|
|
125
|
-
timestamp: number;
|
|
126
|
-
treeId?: string;
|
|
127
|
-
issues: GuardrailIssue[];
|
|
128
|
-
hotPaths: HotPath[];
|
|
129
|
-
budgets: BudgetStatus;
|
|
130
|
-
stats: RuntimeStats;
|
|
131
|
-
recommendations: string[];
|
|
132
|
-
}
|
|
133
|
-
export interface GuardrailsAPI {
|
|
134
|
-
getReport(): GuardrailsReport;
|
|
135
|
-
getStats(): RuntimeStats;
|
|
136
|
-
suppress(fn: () => void): void;
|
|
137
|
-
dispose(): void;
|
|
138
|
-
}
|
package/src/noop.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { SignalTree } from '@signaltree/core';
|
|
2
|
-
import type { GuardrailsConfig, GuardrailRule } from './lib/types';
|
|
3
|
-
export declare function withGuardrails<T extends Record<string, unknown>>(config?: GuardrailsConfig): (tree: SignalTree<T>) => SignalTree<T>;
|
|
4
|
-
export declare const rules: {
|
|
5
|
-
noDeepNesting: () => GuardrailRule<Record<string, unknown>>;
|
|
6
|
-
noFunctionsInState: () => GuardrailRule<Record<string, unknown>>;
|
|
7
|
-
noCacheInPersistence: () => GuardrailRule<Record<string, unknown>>;
|
|
8
|
-
maxPayloadSize: () => GuardrailRule<Record<string, unknown>>;
|
|
9
|
-
noSensitiveData: () => GuardrailRule<Record<string, unknown>>;
|
|
10
|
-
};
|
|
11
|
-
export type * from './lib/types';
|