@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
|
@@ -0,0 +1,62 @@
|
|
|
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.d.ts
CHANGED
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { G as GuardrailsConfig, a as GuardrailRule } from './types-DfZ9n1yX.js';
|
|
3
|
-
export { d as BudgetItem, B as BudgetStatus, c as GuardrailIssue, f as GuardrailsAPI, e as GuardrailsReport, H as HotPath, R as RuleContext, b as RuntimeStats, U as UpdateMetadata } from './types-DfZ9n1yX.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Production no-op module
|
|
7
|
-
* This module exports empty implementations to ensure zero production cost
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
declare function withGuardrails<T extends Record<string, unknown>>(config?: GuardrailsConfig): (tree: SignalTree<T>) => SignalTree<T>;
|
|
11
|
-
declare const rules: {
|
|
12
|
-
noDeepNesting: () => GuardrailRule<Record<string, unknown>>;
|
|
13
|
-
noFunctionsInState: () => GuardrailRule<Record<string, unknown>>;
|
|
14
|
-
noCacheInPersistence: () => GuardrailRule<Record<string, unknown>>;
|
|
15
|
-
maxPayloadSize: () => GuardrailRule<Record<string, unknown>>;
|
|
16
|
-
noSensitiveData: () => GuardrailRule<Record<string, unknown>>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export { GuardrailRule, GuardrailsConfig, rules, withGuardrails };
|
|
1
|
+
export * from "./src/noop";
|
package/dist/noop.js
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
|
|
4
|
-
// src/noop.ts
|
|
5
|
-
var noopRule = /* @__PURE__ */ __name((name) => ({
|
|
1
|
+
const noopRule = name => ({
|
|
6
2
|
name,
|
|
7
|
-
description:
|
|
8
|
-
test:
|
|
9
|
-
message:
|
|
10
|
-
severity:
|
|
11
|
-
})
|
|
3
|
+
description: 'No-op guardrail',
|
|
4
|
+
test: () => true,
|
|
5
|
+
message: '',
|
|
6
|
+
severity: 'info'
|
|
7
|
+
});
|
|
12
8
|
function withGuardrails(config) {
|
|
13
|
-
return
|
|
9
|
+
return tree => {
|
|
14
10
|
return tree;
|
|
15
11
|
};
|
|
16
12
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
noSensitiveData: /* @__PURE__ */ __name(() => noopRule("noop"), "noSensitiveData")
|
|
13
|
+
const rules = {
|
|
14
|
+
noDeepNesting: () => noopRule('noop'),
|
|
15
|
+
noFunctionsInState: () => noopRule('noop'),
|
|
16
|
+
noCacheInPersistence: () => noopRule('noop'),
|
|
17
|
+
maxPayloadSize: () => noopRule('noop'),
|
|
18
|
+
noSensitiveData: () => noopRule('noop')
|
|
24
19
|
};
|
|
25
20
|
|
|
26
21
|
export { rules, withGuardrails };
|
|
27
|
-
//# sourceMappingURL=noop.js.map
|
|
28
|
-
//# sourceMappingURL=noop.js.map
|
package/package.json
CHANGED
|
@@ -1,60 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/guardrails",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Development-only performance monitoring and anti-pattern detection for SignalTree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
|
-
"main": "./dist/
|
|
8
|
-
"module": "./dist/
|
|
9
|
-
"types": "./
|
|
7
|
+
"main": "./dist/lib/guardrails.js",
|
|
8
|
+
"module": "./dist/lib/guardrails.js",
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"types": "./
|
|
12
|
+
"types": "./src/index.d.ts",
|
|
13
13
|
"development": {
|
|
14
|
-
"import": "./dist/
|
|
15
|
-
"
|
|
14
|
+
"import": "./dist/lib/guardrails.js",
|
|
15
|
+
"default": "./dist/lib/guardrails.js"
|
|
16
16
|
},
|
|
17
17
|
"production": {
|
|
18
18
|
"import": "./dist/noop.js",
|
|
19
|
-
"
|
|
19
|
+
"default": "./dist/noop.js"
|
|
20
20
|
},
|
|
21
21
|
"default": "./dist/noop.js"
|
|
22
22
|
},
|
|
23
23
|
"./factories": {
|
|
24
|
-
"types": "./
|
|
24
|
+
"types": "./src/factories/index.d.ts",
|
|
25
25
|
"development": {
|
|
26
26
|
"import": "./dist/factories/index.js",
|
|
27
|
-
"
|
|
27
|
+
"default": "./dist/factories/index.js"
|
|
28
28
|
},
|
|
29
29
|
"production": {
|
|
30
30
|
"import": "./dist/noop.js",
|
|
31
|
-
"
|
|
31
|
+
"default": "./dist/noop.js"
|
|
32
32
|
},
|
|
33
33
|
"default": "./dist/noop.js"
|
|
34
34
|
},
|
|
35
|
+
"./noop": {
|
|
36
|
+
"types": "./src/noop.d.ts",
|
|
37
|
+
"import": "./dist/noop.js",
|
|
38
|
+
"default": "./dist/noop.js"
|
|
39
|
+
},
|
|
35
40
|
"./package.json": "./package.json"
|
|
36
41
|
},
|
|
37
42
|
"files": [
|
|
38
43
|
"dist",
|
|
44
|
+
"src",
|
|
39
45
|
"README.md",
|
|
40
46
|
"CHANGELOG.md"
|
|
41
47
|
],
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
42
51
|
"scripts": {
|
|
43
|
-
"build": "
|
|
44
|
-
"test": "
|
|
45
|
-
"
|
|
46
|
-
"test:
|
|
47
|
-
"
|
|
48
|
-
"type-check": "tsc --noEmit"
|
|
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"
|
|
49
58
|
},
|
|
50
59
|
"peerDependencies": {
|
|
51
|
-
"@signaltree/core": "^4.
|
|
60
|
+
"@signaltree/core": "^4.1.0",
|
|
61
|
+
"tslib": "^2.0.0"
|
|
52
62
|
},
|
|
53
63
|
"devDependencies": {
|
|
54
64
|
"@signaltree/core": "workspace:*",
|
|
55
65
|
"@signaltree/shared": "workspace:*",
|
|
56
|
-
"@signaltree/types": "workspace:*"
|
|
57
|
-
"tsup": "^8.0.0"
|
|
66
|
+
"@signaltree/types": "workspace:*"
|
|
58
67
|
},
|
|
59
68
|
"keywords": [
|
|
60
69
|
"signaltree",
|
|
@@ -0,0 +1,20 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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';
|