@signaltree/guardrails 4.0.16 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/factories/index.d.ts +1 -48
- package/dist/factories/index.js +15 -810
- package/dist/{index.js → lib/guardrails.js} +94 -262
- package/dist/lib/rules.js +62 -0
- package/dist/noop.d.ts +1 -19
- package/dist/noop.js +13 -20
- package/package.json +29 -20
- package/src/factories/index.d.ts +20 -0
- package/src/index.d.ts +4 -0
- package/src/lib/guardrails.d.ts +3 -0
- package/src/lib/rules.d.ts +8 -0
- package/src/lib/types.d.ts +138 -0
- package/src/noop.d.ts +11 -0
- package/dist/factories/index.cjs +0 -922
- package/dist/factories/index.cjs.map +0 -1
- package/dist/factories/index.d.cts +0 -48
- package/dist/factories/index.js.map +0 -1
- package/dist/index.cjs +0 -783
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -44
- package/dist/index.d.ts +0 -44
- package/dist/index.js.map +0 -1
- package/dist/noop.cjs +0 -31
- package/dist/noop.cjs.map +0 -1
- package/dist/noop.d.cts +0 -19
- package/dist/noop.js.map +0 -1
- package/dist/types-DfZ9n1yX.d.cts +0 -255
- package/dist/types-DfZ9n1yX.d.ts +0 -255
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
|
|
4
|
-
// src/lib/guardrails.ts
|
|
5
1
|
function isFunction(value) {
|
|
6
|
-
return typeof value ===
|
|
2
|
+
return typeof value === 'function';
|
|
7
3
|
}
|
|
8
|
-
__name(isFunction, "isFunction");
|
|
9
4
|
function isString(value) {
|
|
10
|
-
return typeof value ===
|
|
5
|
+
return typeof value === 'string';
|
|
11
6
|
}
|
|
12
|
-
__name(isString, "isString");
|
|
13
7
|
function isObjectLike(value) {
|
|
14
|
-
return typeof value ===
|
|
8
|
+
return typeof value === 'object' && value !== null;
|
|
15
9
|
}
|
|
16
|
-
__name(isObjectLike, "isObjectLike");
|
|
17
10
|
function resolveEnabledFlag(option) {
|
|
18
|
-
if (option ===
|
|
11
|
+
if (option === undefined) {
|
|
19
12
|
return true;
|
|
20
13
|
}
|
|
21
14
|
if (isFunction(option)) {
|
|
@@ -27,40 +20,35 @@ function resolveEnabledFlag(option) {
|
|
|
27
20
|
}
|
|
28
21
|
return option;
|
|
29
22
|
}
|
|
30
|
-
__name(resolveEnabledFlag, "resolveEnabledFlag");
|
|
31
23
|
function supportsMiddleware(tree) {
|
|
32
24
|
const candidate = tree;
|
|
33
25
|
return isFunction(candidate.addTap) && isFunction(candidate.removeTap);
|
|
34
26
|
}
|
|
35
|
-
__name(supportsMiddleware, "supportsMiddleware");
|
|
36
27
|
function tryStructuredClone(value) {
|
|
37
28
|
const cloneFn = globalThis.structuredClone;
|
|
38
29
|
if (isFunction(cloneFn)) {
|
|
39
30
|
try {
|
|
40
31
|
return cloneFn(value);
|
|
41
|
-
} catch {
|
|
42
|
-
}
|
|
32
|
+
} catch {}
|
|
43
33
|
}
|
|
44
34
|
return value;
|
|
45
35
|
}
|
|
46
|
-
__name(tryStructuredClone, "tryStructuredClone");
|
|
47
36
|
function isDevEnvironment() {
|
|
48
|
-
if (__DEV__ !==
|
|
49
|
-
if (process?.env?.[
|
|
37
|
+
if (__DEV__ !== undefined) return __DEV__;
|
|
38
|
+
if (process?.env?.['NODE_ENV'] === 'production') return false;
|
|
50
39
|
if (ngDevMode != null) return Boolean(ngDevMode);
|
|
51
40
|
return true;
|
|
52
41
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
var RECOMPUTATION_WINDOW_MS = 1e3;
|
|
42
|
+
const MAX_TIMING_SAMPLES = 1000;
|
|
43
|
+
const RECOMPUTATION_WINDOW_MS = 1000;
|
|
56
44
|
function withGuardrails(config = {}) {
|
|
57
|
-
return
|
|
45
|
+
return tree => {
|
|
58
46
|
const enabled = resolveEnabledFlag(config.enabled);
|
|
59
47
|
if (!isDevEnvironment() || !enabled) {
|
|
60
48
|
return tree;
|
|
61
49
|
}
|
|
62
50
|
if (!supportsMiddleware(tree)) {
|
|
63
|
-
console.warn(
|
|
51
|
+
console.warn('[Guardrails] Tree does not expose middleware hooks; guardrails disabled.');
|
|
64
52
|
return tree;
|
|
65
53
|
}
|
|
66
54
|
const stats = createRuntimeStats();
|
|
@@ -73,27 +61,26 @@ function withGuardrails(config = {}) {
|
|
|
73
61
|
currentUpdate: null,
|
|
74
62
|
suppressed: false,
|
|
75
63
|
timings: [],
|
|
76
|
-
hotPathData:
|
|
77
|
-
issueMap:
|
|
78
|
-
signalUsage:
|
|
64
|
+
hotPathData: new Map(),
|
|
65
|
+
issueMap: new Map(),
|
|
66
|
+
signalUsage: new Map(),
|
|
79
67
|
memoryHistory: [],
|
|
80
68
|
recomputationLog: [],
|
|
81
69
|
disposed: false
|
|
82
70
|
};
|
|
83
|
-
const middlewareId = `guardrails:${config.treeId ??
|
|
71
|
+
const middlewareId = `guardrails:${config.treeId ?? 'tree'}:${Math.random().toString(36).slice(2)}`;
|
|
84
72
|
context.middlewareId = middlewareId;
|
|
85
73
|
const middleware = createGuardrailsMiddleware(context);
|
|
86
74
|
tree.addTap(middleware);
|
|
87
75
|
const stopMonitoring = startMonitoring(context);
|
|
88
|
-
const teardown =
|
|
76
|
+
const teardown = () => {
|
|
89
77
|
if (context.disposed) return;
|
|
90
78
|
context.disposed = true;
|
|
91
79
|
stopMonitoring();
|
|
92
80
|
try {
|
|
93
81
|
tree.removeTap(middlewareId);
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
96
|
-
}, "teardown");
|
|
82
|
+
} catch {}
|
|
83
|
+
};
|
|
97
84
|
const originalDestroy = tree.destroy?.bind(tree);
|
|
98
85
|
tree.destroy = () => {
|
|
99
86
|
teardown();
|
|
@@ -101,11 +88,10 @@ function withGuardrails(config = {}) {
|
|
|
101
88
|
originalDestroy();
|
|
102
89
|
}
|
|
103
90
|
};
|
|
104
|
-
tree[
|
|
91
|
+
tree['__guardrails'] = createAPI(context, teardown);
|
|
105
92
|
return tree;
|
|
106
93
|
};
|
|
107
94
|
}
|
|
108
|
-
__name(withGuardrails, "withGuardrails");
|
|
109
95
|
function createRuntimeStats() {
|
|
110
96
|
return {
|
|
111
97
|
updateCount: 0,
|
|
@@ -125,11 +111,10 @@ function createRuntimeStats() {
|
|
|
125
111
|
violationCount: 0
|
|
126
112
|
};
|
|
127
113
|
}
|
|
128
|
-
__name(createRuntimeStats, "createRuntimeStats");
|
|
129
114
|
function createGuardrailsMiddleware(context) {
|
|
130
115
|
return {
|
|
131
|
-
id: context.middlewareId ??
|
|
132
|
-
before:
|
|
116
|
+
id: context.middlewareId ?? 'guardrails',
|
|
117
|
+
before: (action, payload, state) => {
|
|
133
118
|
if (context.suppressed) {
|
|
134
119
|
context.currentUpdate = null;
|
|
135
120
|
return !context.disposed;
|
|
@@ -150,8 +135,8 @@ function createGuardrailsMiddleware(context) {
|
|
|
150
135
|
analyzePreUpdate(context, detail, metadata);
|
|
151
136
|
}
|
|
152
137
|
return !context.disposed;
|
|
153
|
-
},
|
|
154
|
-
after:
|
|
138
|
+
},
|
|
139
|
+
after: (_action, _payload, _previousState, newState) => {
|
|
155
140
|
const pending = context.currentUpdate;
|
|
156
141
|
if (!pending) return;
|
|
157
142
|
const duration = Math.max(0, performance.now() - pending.startTime);
|
|
@@ -168,20 +153,16 @@ function createGuardrailsMiddleware(context) {
|
|
|
168
153
|
updateSignalStats(context, timestamp);
|
|
169
154
|
recordRecomputations(context, recomputations, timestamp);
|
|
170
155
|
context.currentUpdate = null;
|
|
171
|
-
}
|
|
156
|
+
}
|
|
172
157
|
};
|
|
173
158
|
}
|
|
174
|
-
__name(createGuardrailsMiddleware, "createGuardrailsMiddleware");
|
|
175
159
|
function updatePercentiles(context) {
|
|
176
160
|
if (context.timings.length === 0) return;
|
|
177
|
-
const sorted = [
|
|
178
|
-
...context.timings
|
|
179
|
-
].sort((a, b) => a - b);
|
|
161
|
+
const sorted = [...context.timings].sort((a, b) => a - b);
|
|
180
162
|
context.stats.p50UpdateTime = sorted[Math.floor(sorted.length * 0.5)] || 0;
|
|
181
163
|
context.stats.p95UpdateTime = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
|
182
164
|
context.stats.p99UpdateTime = sorted[Math.floor(sorted.length * 0.99)] || 0;
|
|
183
165
|
}
|
|
184
|
-
__name(updatePercentiles, "updatePercentiles");
|
|
185
166
|
function calculateDiffRatio(oldValue, newValue) {
|
|
186
167
|
if (!isComparableRecord(oldValue) || !isComparableRecord(newValue)) {
|
|
187
168
|
return Object.is(oldValue, newValue) ? 0 : 1;
|
|
@@ -189,10 +170,7 @@ function calculateDiffRatio(oldValue, newValue) {
|
|
|
189
170
|
if (oldValue === newValue) return 0;
|
|
190
171
|
const oldKeys = new Set(Object.keys(oldValue));
|
|
191
172
|
const newKeys = new Set(Object.keys(newValue));
|
|
192
|
-
const allKeys =
|
|
193
|
-
...oldKeys,
|
|
194
|
-
...newKeys
|
|
195
|
-
]);
|
|
173
|
+
const allKeys = new Set([...oldKeys, ...newKeys]);
|
|
196
174
|
let changed = 0;
|
|
197
175
|
for (const key of allKeys) {
|
|
198
176
|
if (!oldKeys.has(key) || !newKeys.has(key) || oldValue[key] !== newValue[key]) {
|
|
@@ -201,7 +179,6 @@ function calculateDiffRatio(oldValue, newValue) {
|
|
|
201
179
|
}
|
|
202
180
|
return allKeys.size === 0 ? 0 : changed / allKeys.size;
|
|
203
181
|
}
|
|
204
|
-
__name(calculateDiffRatio, "calculateDiffRatio");
|
|
205
182
|
function analyzePreUpdate(context, detail, metadata) {
|
|
206
183
|
if (!context.config.customRules) return;
|
|
207
184
|
for (const rule of context.config.customRules) {
|
|
@@ -215,12 +192,11 @@ function analyzePreUpdate(context, detail, metadata) {
|
|
|
215
192
|
});
|
|
216
193
|
}
|
|
217
194
|
}
|
|
218
|
-
__name(analyzePreUpdate, "analyzePreUpdate");
|
|
219
195
|
function analyzePostUpdate(context, detail, duration, diffRatio, isPrimary) {
|
|
220
196
|
if (isPrimary && context.config.budgets?.maxUpdateTime && duration > context.config.budgets.maxUpdateTime) {
|
|
221
197
|
addIssue(context, {
|
|
222
|
-
type:
|
|
223
|
-
severity:
|
|
198
|
+
type: 'budget',
|
|
199
|
+
severity: 'error',
|
|
224
200
|
message: `Update took ${duration.toFixed(2)}ms (budget: ${context.config.budgets.maxUpdateTime}ms)`,
|
|
225
201
|
path: detail.path,
|
|
226
202
|
count: 1
|
|
@@ -229,8 +205,8 @@ function analyzePostUpdate(context, detail, duration, diffRatio, isPrimary) {
|
|
|
229
205
|
const minDiff = context.config.analysis?.minDiffForParentReplace ?? 0.8;
|
|
230
206
|
if (context.config.analysis?.warnParentReplace && diffRatio > minDiff) {
|
|
231
207
|
addIssue(context, {
|
|
232
|
-
type:
|
|
233
|
-
severity:
|
|
208
|
+
type: 'analysis',
|
|
209
|
+
severity: 'warning',
|
|
234
210
|
message: `High diff ratio (${(diffRatio * 100).toFixed(0)}%) - consider scoped updates`,
|
|
235
211
|
path: detail.path,
|
|
236
212
|
count: 1,
|
|
@@ -238,12 +214,11 @@ function analyzePostUpdate(context, detail, duration, diffRatio, isPrimary) {
|
|
|
238
214
|
});
|
|
239
215
|
}
|
|
240
216
|
}
|
|
241
|
-
__name(analyzePostUpdate, "analyzePostUpdate");
|
|
242
217
|
function trackHotPath(context, path, duration) {
|
|
243
218
|
if (!context.config.hotPaths?.enabled) return;
|
|
244
|
-
const pathKey = Array.isArray(path) ? path.join(
|
|
219
|
+
const pathKey = Array.isArray(path) ? path.join('.') : path;
|
|
245
220
|
const now = Date.now();
|
|
246
|
-
const windowMs = context.config.hotPaths.windowMs ||
|
|
221
|
+
const windowMs = context.config.hotPaths.windowMs || 1000;
|
|
247
222
|
let data = context.hotPathData.get(pathKey);
|
|
248
223
|
if (!data) {
|
|
249
224
|
data = {
|
|
@@ -261,11 +236,9 @@ function trackHotPath(context, path, duration) {
|
|
|
261
236
|
data.durations.push(duration);
|
|
262
237
|
data.lastUpdate = now;
|
|
263
238
|
const threshold = context.config.hotPaths.threshold || 10;
|
|
264
|
-
const updatesPerSecond = data.count / windowMs *
|
|
239
|
+
const updatesPerSecond = data.count / windowMs * 1000;
|
|
265
240
|
if (updatesPerSecond > threshold) {
|
|
266
|
-
const sorted = [
|
|
267
|
-
...data.durations
|
|
268
|
-
].sort((a, b) => a - b);
|
|
241
|
+
const sorted = [...data.durations].sort((a, b) => a - b);
|
|
269
242
|
const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
|
270
243
|
const avg = data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length;
|
|
271
244
|
updateHotPath(context, {
|
|
@@ -278,9 +251,8 @@ function trackHotPath(context, path, duration) {
|
|
|
278
251
|
});
|
|
279
252
|
}
|
|
280
253
|
}
|
|
281
|
-
__name(trackHotPath, "trackHotPath");
|
|
282
254
|
function trackSignalUsage(context, path, timestamp) {
|
|
283
|
-
const key = Array.isArray(path) ? path.join(
|
|
255
|
+
const key = Array.isArray(path) ? path.join('.') : path;
|
|
284
256
|
const entry = context.signalUsage.get(key) ?? {
|
|
285
257
|
updates: 0,
|
|
286
258
|
lastSeen: timestamp
|
|
@@ -289,27 +261,23 @@ function trackSignalUsage(context, path, timestamp) {
|
|
|
289
261
|
entry.lastSeen = timestamp;
|
|
290
262
|
context.signalUsage.set(key, entry);
|
|
291
263
|
}
|
|
292
|
-
__name(trackSignalUsage, "trackSignalUsage");
|
|
293
264
|
function updateSignalStats(context, timestamp) {
|
|
294
|
-
const retentionWindow = context.config.memoryLeaks?.checkInterval ??
|
|
295
|
-
const historyWindow = Math.max(retentionWindow,
|
|
265
|
+
const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5000;
|
|
266
|
+
const historyWindow = Math.max(retentionWindow, 1000);
|
|
296
267
|
const signalCount = context.signalUsage.size;
|
|
297
268
|
context.stats.signalCount = signalCount;
|
|
298
|
-
const staleCount = [
|
|
299
|
-
...context.signalUsage.values()
|
|
300
|
-
].filter((entry) => timestamp - entry.lastSeen > retentionWindow).length;
|
|
269
|
+
const staleCount = [...context.signalUsage.values()].filter(entry => timestamp - entry.lastSeen > retentionWindow).length;
|
|
301
270
|
context.stats.signalRetention = staleCount;
|
|
302
271
|
context.stats.unreadSignalCount = 0;
|
|
303
272
|
context.memoryHistory.push({
|
|
304
273
|
timestamp,
|
|
305
274
|
count: signalCount
|
|
306
275
|
});
|
|
307
|
-
context.memoryHistory = context.memoryHistory.filter(
|
|
276
|
+
context.memoryHistory = context.memoryHistory.filter(entry => timestamp - entry.timestamp <= historyWindow);
|
|
308
277
|
const baseline = context.memoryHistory[0]?.count ?? signalCount;
|
|
309
278
|
const growth = baseline === 0 ? 0 : (signalCount - baseline) / Math.max(1, baseline);
|
|
310
279
|
context.stats.memoryGrowthRate = growth;
|
|
311
280
|
}
|
|
312
|
-
__name(updateSignalStats, "updateSignalStats");
|
|
313
281
|
function recordRecomputations(context, count, timestamp) {
|
|
314
282
|
if (count > 0) {
|
|
315
283
|
context.stats.recomputationCount += count;
|
|
@@ -318,13 +286,12 @@ function recordRecomputations(context, count, timestamp) {
|
|
|
318
286
|
}
|
|
319
287
|
}
|
|
320
288
|
if (context.recomputationLog.length) {
|
|
321
|
-
context.recomputationLog = context.recomputationLog.filter(
|
|
289
|
+
context.recomputationLog = context.recomputationLog.filter(value => timestamp - value <= RECOMPUTATION_WINDOW_MS);
|
|
322
290
|
}
|
|
323
291
|
context.stats.recomputationsPerSecond = context.recomputationLog.length;
|
|
324
292
|
}
|
|
325
|
-
__name(recordRecomputations, "recordRecomputations");
|
|
326
293
|
function updateHotPath(context, hotPath) {
|
|
327
|
-
const existing = context.hotPaths.find(
|
|
294
|
+
const existing = context.hotPaths.find(h => h.path === hotPath.path);
|
|
328
295
|
if (existing) {
|
|
329
296
|
Object.assign(existing, hotPath);
|
|
330
297
|
} else {
|
|
@@ -337,29 +304,28 @@ function updateHotPath(context, hotPath) {
|
|
|
337
304
|
}
|
|
338
305
|
context.stats.hotPathCount = context.hotPaths.length;
|
|
339
306
|
}
|
|
340
|
-
__name(updateHotPath, "updateHotPath");
|
|
341
307
|
function evaluateRule(context, rule, ruleContext) {
|
|
342
|
-
const handleFailure =
|
|
343
|
-
const message = typeof rule.message ===
|
|
308
|
+
const handleFailure = () => {
|
|
309
|
+
const message = typeof rule.message === 'function' ? rule.message(ruleContext) : rule.message;
|
|
344
310
|
addIssue(context, {
|
|
345
|
-
type:
|
|
346
|
-
severity: rule.severity ||
|
|
311
|
+
type: 'rule',
|
|
312
|
+
severity: rule.severity || 'warning',
|
|
347
313
|
message,
|
|
348
|
-
path: ruleContext.path.join(
|
|
314
|
+
path: ruleContext.path.join('.'),
|
|
349
315
|
count: 1,
|
|
350
316
|
metadata: {
|
|
351
317
|
rule: rule.name
|
|
352
318
|
}
|
|
353
319
|
});
|
|
354
|
-
}
|
|
320
|
+
};
|
|
355
321
|
try {
|
|
356
322
|
const result = rule.test(ruleContext);
|
|
357
323
|
if (result instanceof Promise) {
|
|
358
|
-
result.then(
|
|
324
|
+
result.then(outcome => {
|
|
359
325
|
if (!outcome) {
|
|
360
326
|
handleFailure();
|
|
361
327
|
}
|
|
362
|
-
}).catch(
|
|
328
|
+
}).catch(error => {
|
|
363
329
|
console.warn(`[Guardrails] Rule ${rule.name} rejected:`, error);
|
|
364
330
|
});
|
|
365
331
|
return;
|
|
@@ -371,7 +337,6 @@ function evaluateRule(context, rule, ruleContext) {
|
|
|
371
337
|
console.warn(`[Guardrails] Rule ${rule.name} threw error:`, error);
|
|
372
338
|
}
|
|
373
339
|
}
|
|
374
|
-
__name(evaluateRule, "evaluateRule");
|
|
375
340
|
function addIssue(context, issue) {
|
|
376
341
|
if (context.suppressed) return;
|
|
377
342
|
if (context.config.reporting?.aggregateWarnings !== false) {
|
|
@@ -385,11 +350,10 @@ function addIssue(context, issue) {
|
|
|
385
350
|
}
|
|
386
351
|
context.issues.push(issue);
|
|
387
352
|
context.stats.violationCount++;
|
|
388
|
-
if (context.config.mode ===
|
|
353
|
+
if (context.config.mode === 'throw') {
|
|
389
354
|
throw new Error(`[Guardrails] ${issue.message}`);
|
|
390
355
|
}
|
|
391
356
|
}
|
|
392
|
-
__name(addIssue, "addIssue");
|
|
393
357
|
function shouldSuppressUpdate(context, metadata) {
|
|
394
358
|
if (context.suppressed) return true;
|
|
395
359
|
if (!metadata) return false;
|
|
@@ -397,12 +361,8 @@ function shouldSuppressUpdate(context, metadata) {
|
|
|
397
361
|
return true;
|
|
398
362
|
}
|
|
399
363
|
const autoSuppress = new Set(context.config.suppression?.autoSuppress ?? []);
|
|
400
|
-
return [
|
|
401
|
-
metadata.intent,
|
|
402
|
-
metadata.source
|
|
403
|
-
].some((value) => isString(value) && autoSuppress.has(value));
|
|
364
|
+
return [metadata.intent, metadata.source].some(value => isString(value) && autoSuppress.has(value));
|
|
404
365
|
}
|
|
405
|
-
__name(shouldSuppressUpdate, "shouldSuppressUpdate");
|
|
406
366
|
function startMonitoring(context) {
|
|
407
367
|
const interval = setInterval(() => {
|
|
408
368
|
if (context.disposed) {
|
|
@@ -411,28 +371,25 @@ function startMonitoring(context) {
|
|
|
411
371
|
}
|
|
412
372
|
checkMemory(context);
|
|
413
373
|
maybeReport(context);
|
|
414
|
-
}, context.config.reporting?.interval ||
|
|
374
|
+
}, context.config.reporting?.interval || 5000);
|
|
415
375
|
return () => clearInterval(interval);
|
|
416
376
|
}
|
|
417
|
-
__name(startMonitoring, "startMonitoring");
|
|
418
377
|
function checkMemory(context) {
|
|
419
378
|
if (!context.config.memoryLeaks?.enabled) return;
|
|
420
379
|
const now = Date.now();
|
|
421
|
-
const retentionWindow = context.config.memoryLeaks?.checkInterval ??
|
|
380
|
+
const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5000;
|
|
422
381
|
const retentionThreshold = context.config.memoryLeaks?.retentionThreshold ?? 100;
|
|
423
382
|
const growthThreshold = context.config.memoryLeaks?.growthRate ?? 0.2;
|
|
424
|
-
const staleCount = [
|
|
425
|
-
...context.signalUsage.values()
|
|
426
|
-
].filter((entry) => now - entry.lastSeen > retentionWindow).length;
|
|
383
|
+
const staleCount = [...context.signalUsage.values()].filter(entry => now - entry.lastSeen > retentionWindow).length;
|
|
427
384
|
context.stats.signalRetention = staleCount;
|
|
428
385
|
const exceedsRetention = context.stats.signalRetention > retentionThreshold;
|
|
429
386
|
const exceedsGrowth = context.stats.memoryGrowthRate > growthThreshold;
|
|
430
387
|
if (exceedsRetention || exceedsGrowth) {
|
|
431
388
|
addIssue(context, {
|
|
432
|
-
type:
|
|
433
|
-
severity:
|
|
389
|
+
type: 'memory',
|
|
390
|
+
severity: 'warning',
|
|
434
391
|
message: `Potential memory leak detected (signals: ${context.stats.signalCount}, growth ${(context.stats.memoryGrowthRate * 100).toFixed(1)}%)`,
|
|
435
|
-
path:
|
|
392
|
+
path: 'root',
|
|
436
393
|
count: 1,
|
|
437
394
|
metadata: {
|
|
438
395
|
signalCount: context.stats.signalCount,
|
|
@@ -441,7 +398,6 @@ function checkMemory(context) {
|
|
|
441
398
|
});
|
|
442
399
|
}
|
|
443
400
|
}
|
|
444
|
-
__name(checkMemory, "checkMemory");
|
|
445
401
|
function maybeReport(context) {
|
|
446
402
|
if (context.config.reporting?.console === false) return;
|
|
447
403
|
const report = generateReport(context);
|
|
@@ -449,14 +405,13 @@ function maybeReport(context) {
|
|
|
449
405
|
context.config.reporting.customReporter(report);
|
|
450
406
|
}
|
|
451
407
|
if (context.config.reporting?.console && context.issues.length > 0) {
|
|
452
|
-
reportToConsole(report, context.config.reporting.console ===
|
|
408
|
+
reportToConsole(report, context.config.reporting.console === 'verbose');
|
|
453
409
|
}
|
|
454
410
|
context.issues = [];
|
|
455
411
|
context.issueMap.clear();
|
|
456
412
|
}
|
|
457
|
-
__name(maybeReport, "maybeReport");
|
|
458
413
|
function reportToConsole(report, verbose) {
|
|
459
|
-
console.group(
|
|
414
|
+
console.group('[Guardrails] Performance Report');
|
|
460
415
|
logIssues(report.issues);
|
|
461
416
|
logHotPaths(report.hotPaths);
|
|
462
417
|
if (verbose) {
|
|
@@ -464,39 +419,33 @@ function reportToConsole(report, verbose) {
|
|
|
464
419
|
}
|
|
465
420
|
console.groupEnd();
|
|
466
421
|
}
|
|
467
|
-
__name(reportToConsole, "reportToConsole");
|
|
468
422
|
function logIssues(issues) {
|
|
469
423
|
if (issues.length === 0) return;
|
|
470
424
|
console.warn(`${issues.length} issues detected:`);
|
|
471
425
|
for (const issue of issues) {
|
|
472
426
|
const prefix = getSeverityPrefix(issue.severity);
|
|
473
|
-
const countSuffix = issue.count > 1 ? ` (x${issue.count})` :
|
|
427
|
+
const countSuffix = issue.count > 1 ? ` (x${issue.count})` : '';
|
|
474
428
|
const message = `${prefix} [${issue.type}] ${issue.message}${countSuffix}`;
|
|
475
429
|
console.log(` ${message}`);
|
|
476
430
|
}
|
|
477
431
|
}
|
|
478
|
-
__name(logIssues, "logIssues");
|
|
479
432
|
function logHotPaths(hotPaths) {
|
|
480
433
|
if (hotPaths.length === 0) return;
|
|
481
|
-
console.log(
|
|
482
|
-
Hot Paths (${hotPaths.length}):`);
|
|
434
|
+
console.log(`\nHot Paths (${hotPaths.length}):`);
|
|
483
435
|
for (const hp of hotPaths) {
|
|
484
|
-
const entry = `
|
|
436
|
+
const entry = ` 🔥 ${hp.path}: ${hp.updatesPerSecond.toFixed(1)}/s (heat: ${hp.heatScore.toFixed(0)})`;
|
|
485
437
|
console.log(entry);
|
|
486
438
|
}
|
|
487
439
|
}
|
|
488
|
-
__name(logHotPaths, "logHotPaths");
|
|
489
440
|
function logVerboseStats(report) {
|
|
490
|
-
console.log(
|
|
491
|
-
console.log(
|
|
441
|
+
console.log('\nStats:', report.stats);
|
|
442
|
+
console.log('Budgets:', report.budgets);
|
|
492
443
|
}
|
|
493
|
-
__name(logVerboseStats, "logVerboseStats");
|
|
494
444
|
function getSeverityPrefix(severity) {
|
|
495
|
-
if (severity ===
|
|
496
|
-
if (severity ===
|
|
497
|
-
return
|
|
445
|
+
if (severity === 'error') return '❌';
|
|
446
|
+
if (severity === 'warning') return '⚠️';
|
|
447
|
+
return 'ℹ️';
|
|
498
448
|
}
|
|
499
|
-
__name(getSeverityPrefix, "getSeverityPrefix");
|
|
500
449
|
function generateReport(context) {
|
|
501
450
|
const memoryCurrent = context.stats.signalCount;
|
|
502
451
|
const memoryLimit = context.config.budgets?.maxMemory ?? 50;
|
|
@@ -517,25 +466,24 @@ function generateReport(context) {
|
|
|
517
466
|
recommendations: generateRecommendations(context)
|
|
518
467
|
};
|
|
519
468
|
}
|
|
520
|
-
__name(generateReport, "generateReport");
|
|
521
469
|
function createBudgetItem(current, limit) {
|
|
522
470
|
if (limit <= 0) {
|
|
523
471
|
return {
|
|
524
472
|
current,
|
|
525
473
|
limit,
|
|
526
474
|
usage: 0,
|
|
527
|
-
status:
|
|
475
|
+
status: 'ok'
|
|
528
476
|
};
|
|
529
477
|
}
|
|
530
478
|
const usage = current / limit * 100;
|
|
531
479
|
const threshold = 80;
|
|
532
480
|
let status;
|
|
533
481
|
if (usage > 100) {
|
|
534
|
-
status =
|
|
482
|
+
status = 'exceeded';
|
|
535
483
|
} else if (usage > threshold) {
|
|
536
|
-
status =
|
|
484
|
+
status = 'warning';
|
|
537
485
|
} else {
|
|
538
|
-
status =
|
|
486
|
+
status = 'ok';
|
|
539
487
|
}
|
|
540
488
|
return {
|
|
541
489
|
current,
|
|
@@ -544,23 +492,21 @@ function createBudgetItem(current, limit) {
|
|
|
544
492
|
status
|
|
545
493
|
};
|
|
546
494
|
}
|
|
547
|
-
__name(createBudgetItem, "createBudgetItem");
|
|
548
495
|
function generateRecommendations(context) {
|
|
549
496
|
const recommendations = [];
|
|
550
497
|
if (context.hotPaths.length > 0) {
|
|
551
|
-
recommendations.push(
|
|
498
|
+
recommendations.push('Consider batching or debouncing updates to hot paths');
|
|
552
499
|
}
|
|
553
500
|
if (context.stats.avgUpdateTime > 10) {
|
|
554
|
-
recommendations.push(
|
|
501
|
+
recommendations.push('Average update time is high - review update logic');
|
|
555
502
|
}
|
|
556
503
|
return recommendations;
|
|
557
504
|
}
|
|
558
|
-
__name(generateRecommendations, "generateRecommendations");
|
|
559
505
|
function createAPI(context, teardown) {
|
|
560
506
|
return {
|
|
561
|
-
getReport:
|
|
562
|
-
getStats:
|
|
563
|
-
suppress:
|
|
507
|
+
getReport: () => generateReport(context),
|
|
508
|
+
getStats: () => context.stats,
|
|
509
|
+
suppress: fn => {
|
|
564
510
|
const was = context.suppressed;
|
|
565
511
|
context.suppressed = true;
|
|
566
512
|
try {
|
|
@@ -568,62 +514,53 @@ function createAPI(context, teardown) {
|
|
|
568
514
|
} finally {
|
|
569
515
|
context.suppressed = was;
|
|
570
516
|
}
|
|
571
|
-
},
|
|
572
|
-
dispose:
|
|
517
|
+
},
|
|
518
|
+
dispose: () => {
|
|
573
519
|
teardown();
|
|
574
520
|
const finalReport = generateReport(context);
|
|
575
521
|
if (context.config.reporting?.console !== false) {
|
|
576
|
-
console.log(
|
|
522
|
+
console.log('[Guardrails] Final report on disposal:', finalReport);
|
|
577
523
|
}
|
|
578
524
|
if (context.config.reporting?.customReporter) {
|
|
579
525
|
context.config.reporting.customReporter(finalReport);
|
|
580
526
|
}
|
|
581
|
-
}
|
|
527
|
+
}
|
|
582
528
|
};
|
|
583
529
|
}
|
|
584
|
-
__name(createAPI, "createAPI");
|
|
585
530
|
function extractMetadata(payload) {
|
|
586
|
-
if (!isObjectLike(payload)) return
|
|
587
|
-
const candidate = payload[
|
|
588
|
-
return isObjectLike(candidate) ? candidate :
|
|
531
|
+
if (!isObjectLike(payload)) return undefined;
|
|
532
|
+
const candidate = payload['metadata'];
|
|
533
|
+
return isObjectLike(candidate) ? candidate : undefined;
|
|
589
534
|
}
|
|
590
|
-
__name(extractMetadata, "extractMetadata");
|
|
591
535
|
function collectUpdateDetails(payload, stateSnapshot) {
|
|
592
536
|
const details = [];
|
|
593
|
-
const visit =
|
|
594
|
-
const path = segments.length ? segments.join(
|
|
537
|
+
const visit = (value, segments, currentState) => {
|
|
538
|
+
const path = segments.length ? segments.join('.') : 'root';
|
|
595
539
|
const oldValue = captureValue(currentState);
|
|
596
540
|
if (isObjectLike(value)) {
|
|
597
541
|
details.push({
|
|
598
542
|
path,
|
|
599
|
-
segments: [
|
|
600
|
-
...segments
|
|
601
|
-
],
|
|
543
|
+
segments: [...segments],
|
|
602
544
|
newValue: value,
|
|
603
545
|
oldValue
|
|
604
546
|
});
|
|
605
547
|
for (const [key, child] of Object.entries(value)) {
|
|
606
|
-
visit(child, [
|
|
607
|
-
...segments,
|
|
608
|
-
key
|
|
609
|
-
], isObjectLike(currentState) ? currentState[key] : void 0);
|
|
548
|
+
visit(child, [...segments, key], isObjectLike(currentState) ? currentState[key] : undefined);
|
|
610
549
|
}
|
|
611
550
|
return;
|
|
612
551
|
}
|
|
613
552
|
details.push({
|
|
614
553
|
path,
|
|
615
|
-
segments: [
|
|
616
|
-
...segments
|
|
617
|
-
],
|
|
554
|
+
segments: [...segments],
|
|
618
555
|
newValue: value,
|
|
619
556
|
oldValue
|
|
620
557
|
});
|
|
621
|
-
}
|
|
558
|
+
};
|
|
622
559
|
if (isObjectLike(payload)) {
|
|
623
560
|
visit(payload, [], stateSnapshot);
|
|
624
561
|
} else {
|
|
625
562
|
details.push({
|
|
626
|
-
path:
|
|
563
|
+
path: 'root',
|
|
627
564
|
segments: [],
|
|
628
565
|
newValue: payload,
|
|
629
566
|
oldValue: captureValue(stateSnapshot)
|
|
@@ -631,7 +568,7 @@ function collectUpdateDetails(payload, stateSnapshot) {
|
|
|
631
568
|
}
|
|
632
569
|
if (details.length === 0) {
|
|
633
570
|
details.push({
|
|
634
|
-
path:
|
|
571
|
+
path: 'root',
|
|
635
572
|
segments: [],
|
|
636
573
|
newValue: payload,
|
|
637
574
|
oldValue: captureValue(stateSnapshot)
|
|
@@ -639,28 +576,24 @@ function collectUpdateDetails(payload, stateSnapshot) {
|
|
|
639
576
|
}
|
|
640
577
|
return details;
|
|
641
578
|
}
|
|
642
|
-
__name(collectUpdateDetails, "collectUpdateDetails");
|
|
643
579
|
function getValueAtPath(source, segments) {
|
|
644
580
|
let current = source;
|
|
645
581
|
for (const segment of segments) {
|
|
646
582
|
if (!isObjectLike(current)) {
|
|
647
|
-
return
|
|
583
|
+
return undefined;
|
|
648
584
|
}
|
|
649
585
|
current = current[segment];
|
|
650
586
|
}
|
|
651
587
|
return current;
|
|
652
588
|
}
|
|
653
|
-
__name(getValueAtPath, "getValueAtPath");
|
|
654
589
|
function captureValue(value) {
|
|
655
590
|
return tryStructuredClone(value);
|
|
656
591
|
}
|
|
657
|
-
__name(captureValue, "captureValue");
|
|
658
592
|
function isPlainObject(value) {
|
|
659
|
-
if (!value || typeof value !==
|
|
593
|
+
if (!value || typeof value !== 'object') return false;
|
|
660
594
|
const proto = Object.getPrototypeOf(value);
|
|
661
595
|
return proto === Object.prototype || proto === null;
|
|
662
596
|
}
|
|
663
|
-
__name(isPlainObject, "isPlainObject");
|
|
664
597
|
function updateTimingStats(context, duration) {
|
|
665
598
|
context.timings.push(duration);
|
|
666
599
|
if (context.timings.length > MAX_TIMING_SAMPLES) {
|
|
@@ -672,109 +605,8 @@ function updateTimingStats(context, duration) {
|
|
|
672
605
|
context.stats.maxUpdateTime = Math.max(context.stats.maxUpdateTime, duration);
|
|
673
606
|
updatePercentiles(context);
|
|
674
607
|
}
|
|
675
|
-
__name(updateTimingStats, "updateTimingStats");
|
|
676
608
|
function isComparableRecord(value) {
|
|
677
609
|
return isPlainObject(value);
|
|
678
610
|
}
|
|
679
|
-
__name(isComparableRecord, "isComparableRecord");
|
|
680
|
-
|
|
681
|
-
// src/lib/rules.ts
|
|
682
|
-
var rules = {
|
|
683
|
-
/**
|
|
684
|
-
* Prevents deep nesting beyond specified depth
|
|
685
|
-
*/
|
|
686
|
-
noDeepNesting: /* @__PURE__ */ __name((maxDepth = 5) => ({
|
|
687
|
-
name: "no-deep-nesting",
|
|
688
|
-
description: `Prevents nesting deeper than ${maxDepth} levels`,
|
|
689
|
-
test: /* @__PURE__ */ __name((ctx) => ctx.path.length <= maxDepth, "test"),
|
|
690
|
-
message: /* @__PURE__ */ __name((ctx) => `Path too deep: ${ctx.path.join(".")} (${ctx.path.length} levels, max: ${maxDepth})`, "message"),
|
|
691
|
-
severity: "warning",
|
|
692
|
-
tags: [
|
|
693
|
-
"architecture",
|
|
694
|
-
"complexity"
|
|
695
|
-
]
|
|
696
|
-
}), "noDeepNesting"),
|
|
697
|
-
/**
|
|
698
|
-
* Prevents storing functions in state (breaks serialization)
|
|
699
|
-
*/
|
|
700
|
-
noFunctionsInState: /* @__PURE__ */ __name(() => ({
|
|
701
|
-
name: "no-functions",
|
|
702
|
-
description: "Functions break serialization",
|
|
703
|
-
test: /* @__PURE__ */ __name((ctx) => typeof ctx.value !== "function", "test"),
|
|
704
|
-
message: "Functions cannot be stored in state (breaks serialization)",
|
|
705
|
-
severity: "error",
|
|
706
|
-
tags: [
|
|
707
|
-
"serialization",
|
|
708
|
-
"data"
|
|
709
|
-
]
|
|
710
|
-
}), "noFunctionsInState"),
|
|
711
|
-
/**
|
|
712
|
-
* Prevents cache from being persisted
|
|
713
|
-
*/
|
|
714
|
-
noCacheInPersistence: /* @__PURE__ */ __name(() => ({
|
|
715
|
-
name: "no-cache-persistence",
|
|
716
|
-
description: "Prevent cache from being persisted",
|
|
717
|
-
test: /* @__PURE__ */ __name((ctx) => {
|
|
718
|
-
if (ctx.metadata?.source === "serialization" && ctx.path.includes("cache")) {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
return true;
|
|
722
|
-
}, "test"),
|
|
723
|
-
message: "Cache should not be persisted",
|
|
724
|
-
severity: "warning",
|
|
725
|
-
tags: [
|
|
726
|
-
"persistence",
|
|
727
|
-
"cache"
|
|
728
|
-
]
|
|
729
|
-
}), "noCacheInPersistence"),
|
|
730
|
-
/**
|
|
731
|
-
* Limits payload size
|
|
732
|
-
*/
|
|
733
|
-
maxPayloadSize: /* @__PURE__ */ __name((maxKB = 100) => ({
|
|
734
|
-
name: "max-payload-size",
|
|
735
|
-
description: `Limit payload size to ${maxKB}KB`,
|
|
736
|
-
test: /* @__PURE__ */ __name((ctx) => {
|
|
737
|
-
try {
|
|
738
|
-
const size = JSON.stringify(ctx.value).length;
|
|
739
|
-
return size < maxKB * 1024;
|
|
740
|
-
} catch {
|
|
741
|
-
return true;
|
|
742
|
-
}
|
|
743
|
-
}, "test"),
|
|
744
|
-
message: /* @__PURE__ */ __name((ctx) => {
|
|
745
|
-
const size = JSON.stringify(ctx.value).length;
|
|
746
|
-
const kb = (size / 1024).toFixed(1);
|
|
747
|
-
return `Payload size ${kb}KB exceeds limit of ${maxKB}KB`;
|
|
748
|
-
}, "message"),
|
|
749
|
-
severity: "warning",
|
|
750
|
-
tags: [
|
|
751
|
-
"performance",
|
|
752
|
-
"data"
|
|
753
|
-
]
|
|
754
|
-
}), "maxPayloadSize"),
|
|
755
|
-
/**
|
|
756
|
-
* Prevents storing sensitive data
|
|
757
|
-
*/
|
|
758
|
-
noSensitiveData: /* @__PURE__ */ __name((sensitiveKeys = [
|
|
759
|
-
"password",
|
|
760
|
-
"token",
|
|
761
|
-
"secret",
|
|
762
|
-
"apiKey"
|
|
763
|
-
]) => ({
|
|
764
|
-
name: "no-sensitive-data",
|
|
765
|
-
description: "Prevents storing sensitive data",
|
|
766
|
-
test: /* @__PURE__ */ __name((ctx) => {
|
|
767
|
-
return !ctx.path.some((segment) => sensitiveKeys.some((key) => typeof segment === "string" && segment.toLowerCase().includes(key.toLowerCase())));
|
|
768
|
-
}, "test"),
|
|
769
|
-
message: /* @__PURE__ */ __name((ctx) => `Sensitive data detected in path: ${ctx.path.join(".")}`, "message"),
|
|
770
|
-
severity: "error",
|
|
771
|
-
tags: [
|
|
772
|
-
"security",
|
|
773
|
-
"data"
|
|
774
|
-
]
|
|
775
|
-
}), "noSensitiveData")
|
|
776
|
-
};
|
|
777
611
|
|
|
778
|
-
export {
|
|
779
|
-
//# sourceMappingURL=index.js.map
|
|
780
|
-
//# sourceMappingURL=index.js.map
|
|
612
|
+
export { withGuardrails };
|