@chaos-maker/core 0.4.0 → 0.5.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.
@@ -1,5 +1,7 @@
1
1
  import { ChaosConfig } from './config';
2
+ import { type ValidateChaosConfigOptions } from './validation';
2
3
  import { ChaosEvent, ChaosEventType, ChaosEventListener } from './events';
4
+ import { RuleGroup } from './groups';
3
5
  /**
4
6
  * Global context ChaosMaker patches against. Must expose at minimum `fetch`
5
7
  * (network chaos), and optionally `XMLHttpRequest` / `WebSocket`. In a
@@ -14,6 +16,12 @@ export interface ChaosMakerOptions {
14
16
  * `window` or `self` explicitly only for cross-context testing.
15
17
  */
16
18
  target?: ChaosTarget;
19
+ /**
20
+ * Forwarded to `validateChaosConfig` during construction. Use to
21
+ * relax unknown-field handling, hook deprecation events, or run custom
22
+ * per-`RuleType` validators.
23
+ */
24
+ validation?: ValidateChaosConfigOptions;
17
25
  }
18
26
  export declare class ChaosMaker {
19
27
  private config;
@@ -32,13 +40,46 @@ export declare class ChaosMaker {
32
40
  private eventSourceHandle?;
33
41
  /** Shared counters keyed by config rule object reference. Shared across fetch + XHR + WS. */
34
42
  private requestCounters;
43
+ /** Rule-group registry. Default-on; default group always exists. */
44
+ private groups;
45
+ /** Positional rule-id map shared across interceptors via emitter.
46
+ * Built lazily — only when debug mode is enabled — so disabled instances
47
+ * pay zero allocation cost. The emitter handles `undefined` ruleIds
48
+ * internally via `?.get(rule)`. */
49
+ private ruleIds?;
50
+ /** Logger fed into the emitter; absent ⇒ debug fast-path no-op. */
51
+ private logger?;
35
52
  constructor(config: ChaosConfig, options?: ChaosMakerOptions);
53
+ private seedGroupsFromRules;
54
+ /** Compute the set of group names currently referenced by any rule. Used by `removeGroup`. */
55
+ private collectReferencedGroups;
36
56
  /** Get the seed used by this instance. Log this on failure to reproduce exact chaos decisions. */
37
57
  getSeed(): number;
38
58
  on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
39
59
  off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
40
60
  getLog(): ChaosEvent[];
41
61
  clearLog(): void;
62
+ /** Enable a rule group at runtime. Auto-creates the group if unknown.
63
+ * Engine state and per-rule counters are preserved — no restart. */
64
+ enableGroup(name: string): void;
65
+ /** Disable a rule group at runtime. Subsequent matches are skipped
66
+ * and a single `rule-group:gated` event is emitted per cycle. */
67
+ disableGroup(name: string): void;
68
+ /** Pre-register a group (typically used to ship one as initially disabled). */
69
+ createGroup(name: string, opts?: {
70
+ enabled?: boolean;
71
+ }): void;
72
+ /** Remove a group from the registry. Throws when still referenced unless
73
+ * `{ force: true }`. `'default'` cannot be removed. */
74
+ removeGroup(name: string, opts?: {
75
+ force?: boolean;
76
+ }): boolean;
77
+ hasGroup(name: string): boolean;
78
+ /** True iff the named group is currently enabled (auto-creates unknown names). */
79
+ getGroupState(name: string): boolean;
80
+ /** Snapshot of every known group as `{ name: enabled }`. */
81
+ getGroupsSnapshot(): Record<string, boolean>;
82
+ listGroups(): RuleGroup[];
42
83
  start(): void;
43
84
  stop(): void;
44
85
  }
@@ -1,7 +1,25 @@
1
1
  import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, RequestCountingOptions, SSECorruptionStrategy, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
2
2
  export declare class ChaosConfigBuilder {
3
3
  private config;
4
+ /** Single-shot group name applied to the next rule pushed and then cleared.
5
+ * Sticky semantics intentionally rejected — silent capture of stale groups
6
+ * is harder to debug than the explicit re-chain. */
7
+ private pendingGroup?;
8
+ /** Queued preset names for `.usePreset(...)`. Silently deduped on
9
+ * push. Flushed onto `out.presets` in `.build()` when non-empty. */
10
+ private pendingPresets;
4
11
  constructor(initialConfig?: ChaosConfig);
12
+ /** Tag the next rule pushed with this group name.
13
+ * Single-shot: cleared after the next builder method that pushes a rule. */
14
+ inGroup(name: string): this;
15
+ /** Pre-register a group on the config (typically used to ship one as
16
+ * initially disabled). Equivalent to setting `ChaosConfig.groups` directly. */
17
+ defineGroup(name: string, opts?: {
18
+ enabled?: boolean;
19
+ }): this;
20
+ /** Apply `pendingGroup` (single-shot) to a rule literal before it is pushed.
21
+ * MUST be called at every rule-push site so `.inGroup(...)` is honored. */
22
+ private withGroup;
5
23
  failRequests(urlPattern: string, statusCode: number, probability: number, methods?: string[], body?: string, headers?: Record<string, string>, counting?: RequestCountingOptions): this;
6
24
  addLatency(urlPattern: string, delayMs: number, probability: number, methods?: string[], counting?: RequestCountingOptions): this;
7
25
  abortRequests(urlPattern: string, probability: number, timeout?: number, methods?: string[], counting?: RequestCountingOptions): this;
@@ -47,5 +65,11 @@ export declare class ChaosConfigBuilder {
47
65
  }, counting?: RequestCountingOptions): this;
48
66
  /** Set the PRNG seed for reproducible chaos. */
49
67
  withSeed(seed: number): this;
68
+ /** Toggle Debug Mode on this config. Off by default. */
69
+ withDebug(enabled?: boolean): this;
70
+ /** Queue a preset name to be expanded at engine init.
71
+ * Silently dedups within the builder, preserving insertion order. Empty
72
+ * / whitespace-only names throw, matching the schema and registry rules. */
73
+ usePreset(name: string): this;
50
74
  build(): ChaosConfig;
51
75
  }
@@ -1,3 +1,5 @@
1
+ import type { RuleGroupConfig } from './groups';
2
+ import type { PresetConfigSlice } from './presets';
1
3
  /** Counting options shared by all network chaos config types.
2
4
  * At most one of `onNth`, `everyNth`, or `afterN` may be set on a single rule.
3
5
  * Counting is per-rule and shared across fetch + XHR (only increments when a
@@ -11,6 +13,14 @@ export interface RequestCountingOptions {
11
13
  everyNth?: number;
12
14
  afterN?: number;
13
15
  }
16
+ /** Optional group membership shared by every rule type.
17
+ * Rules without a `group` belong to the implicit `'default'` group, which is
18
+ * always enabled. Toggling a group at runtime via `enableGroup` /
19
+ * `disableGroup` skips its rules without restarting the engine — counters
20
+ * stay intact across toggles. */
21
+ export interface RuleGroupAssignment {
22
+ group?: string;
23
+ }
14
24
  /** Match a GraphQL operation by name. Applied AFTER `urlPattern` + `methods`
15
25
  * as an additive filter — never a replacement. Matches against:
16
26
  * - JSON `operationName` field on POST request bodies, OR
@@ -33,27 +43,27 @@ export interface NetworkRuleMatchers {
33
43
  methods?: string[];
34
44
  graphqlOperation?: GraphQLOperationMatcher;
35
45
  }
36
- export interface NetworkFailureConfig extends RequestCountingOptions, NetworkRuleMatchers {
46
+ export interface NetworkFailureConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
37
47
  statusCode: number;
38
48
  probability: number;
39
49
  body?: string;
40
50
  statusText?: string;
41
51
  headers?: Record<string, string>;
42
52
  }
43
- export interface NetworkLatencyConfig extends RequestCountingOptions, NetworkRuleMatchers {
53
+ export interface NetworkLatencyConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
44
54
  delayMs: number;
45
55
  probability: number;
46
56
  }
47
- export interface NetworkAbortConfig extends RequestCountingOptions, NetworkRuleMatchers {
57
+ export interface NetworkAbortConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
48
58
  probability: number;
49
59
  timeout?: number;
50
60
  }
51
61
  export type CorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
52
- export interface NetworkCorruptionConfig extends RequestCountingOptions, NetworkRuleMatchers {
62
+ export interface NetworkCorruptionConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
53
63
  probability: number;
54
64
  strategy: CorruptionStrategy;
55
65
  }
56
- export interface NetworkCorsConfig extends RequestCountingOptions, NetworkRuleMatchers {
66
+ export interface NetworkCorsConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
57
67
  probability: number;
58
68
  }
59
69
  export interface NetworkConfig {
@@ -63,7 +73,7 @@ export interface NetworkConfig {
63
73
  corruptions?: NetworkCorruptionConfig[];
64
74
  cors?: NetworkCorsConfig[];
65
75
  }
66
- export interface UiAssaultConfig {
76
+ export interface UiAssaultConfig extends RuleGroupAssignment {
67
77
  selector: string;
68
78
  action: 'disable' | 'hide' | 'remove';
69
79
  probability: number;
@@ -77,12 +87,12 @@ export interface UiConfig {
77
87
  * - `both` = apply independently in either direction.
78
88
  */
79
89
  export type WebSocketDirection = 'inbound' | 'outbound' | 'both';
80
- export interface WebSocketDropConfig extends RequestCountingOptions {
90
+ export interface WebSocketDropConfig extends RequestCountingOptions, RuleGroupAssignment {
81
91
  urlPattern: string;
82
92
  direction: WebSocketDirection;
83
93
  probability: number;
84
94
  }
85
- export interface WebSocketDelayConfig extends RequestCountingOptions {
95
+ export interface WebSocketDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
86
96
  urlPattern: string;
87
97
  direction: WebSocketDirection;
88
98
  delayMs: number;
@@ -95,13 +105,13 @@ export interface WebSocketDelayConfig extends RequestCountingOptions {
95
105
  * emitted with `applied: false`.
96
106
  */
97
107
  export type WebSocketCorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
98
- export interface WebSocketCorruptConfig extends RequestCountingOptions {
108
+ export interface WebSocketCorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
99
109
  urlPattern: string;
100
110
  direction: WebSocketDirection;
101
111
  strategy: WebSocketCorruptionStrategy;
102
112
  probability: number;
103
113
  }
104
- export interface WebSocketCloseConfig extends RequestCountingOptions {
114
+ export interface WebSocketCloseConfig extends RequestCountingOptions, RuleGroupAssignment {
105
115
  urlPattern: string;
106
116
  /**
107
117
  * WebSocket close code. Must be either `1000` (Normal Closure) or in the
@@ -138,24 +148,24 @@ export type SSECorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'w
138
148
  * - `'*'` matches every event regardless of name.
139
149
  */
140
150
  export type SSEEventTypeMatcher = string | '*';
141
- export interface SSEDropConfig extends RequestCountingOptions {
151
+ export interface SSEDropConfig extends RequestCountingOptions, RuleGroupAssignment {
142
152
  urlPattern: string;
143
153
  eventType?: SSEEventTypeMatcher;
144
154
  probability: number;
145
155
  }
146
- export interface SSEDelayConfig extends RequestCountingOptions {
156
+ export interface SSEDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
147
157
  urlPattern: string;
148
158
  eventType?: SSEEventTypeMatcher;
149
159
  delayMs: number;
150
160
  probability: number;
151
161
  }
152
- export interface SSECorruptConfig extends RequestCountingOptions {
162
+ export interface SSECorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
153
163
  urlPattern: string;
154
164
  eventType?: SSEEventTypeMatcher;
155
165
  strategy: SSECorruptionStrategy;
156
166
  probability: number;
157
167
  }
158
- export interface SSECloseConfig extends RequestCountingOptions {
168
+ export interface SSECloseConfig extends RequestCountingOptions, RuleGroupAssignment {
159
169
  urlPattern: string;
160
170
  /** Delay after `open` before dispatching `error` + closing, in ms. Default 0. */
161
171
  afterMs?: number;
@@ -172,6 +182,64 @@ export interface ChaosConfig {
172
182
  ui?: UiConfig;
173
183
  websocket?: WebSocketConfig;
174
184
  sse?: SSEConfig;
185
+ /**
186
+ * Pre-register rule groups with an explicit initial enabled state.
187
+ *
188
+ * Rules opt into a group by setting `group: 'name'`; groups referenced from
189
+ * rules but not listed here are auto-registered as enabled. Use this field
190
+ * only to ship a group as initially disabled (e.g. `{ name: 'payments',
191
+ * enabled: false }`) or to reserve a group name with no rules attached yet.
192
+ */
193
+ groups?: RuleGroupConfig[];
194
+ /**
195
+ * Enable Chaos Maker's structured Debug Mode. When `true`, every
196
+ * rule decision emits a `type: 'debug'` event (with `detail.stage`)
197
+ * through the emitter AND mirrors a `[Chaos] <stage> ...` line to
198
+ * `console.debug`. Framework-agnostic — does not touch
199
+ * Playwright/Cypress/Puppeteer/WDIO debug semantics. Defaults to `false`;
200
+ * fast-path no-op when off.
201
+ *
202
+ * Accepts `boolean` for the common case or `{ enabled: boolean }` to match
203
+ * the `DebugOptions` shape that future Debug Mode extensions (`level`,
204
+ * `prefix`, `console`, `sink`) will add. The validator coerces both forms;
205
+ * the runtime normalizes them via `normalizeDebugOption()`.
206
+ */
207
+ debug?: boolean | {
208
+ enabled: boolean;
209
+ };
210
+ /**
211
+ * Names of presets to expand into this config at engine init.
212
+ * Resolved against the per-instance `PresetRegistry` seeded with built-ins
213
+ * (camelCase names plus the four kebab-case aliases) and any
214
+ * `customPresets` provided alongside this field.
215
+ *
216
+ * Merge semantics: append-only. Each preset's rule arrays concatenate onto
217
+ * the user's rule arrays in the order listed here, preset rules first and
218
+ * user rules last. Duplicate names are silently deduplicated, preserving
219
+ * first occurrence. Unknown names throw `ChaosConfigError` at construction.
220
+ *
221
+ * Preset configs themselves cannot carry `presets` or `customPresets` —
222
+ * dependency chains are out of scope and rejected by the schema.
223
+ */
224
+ presets?: string[];
225
+ /**
226
+ * Per-instance custom presets registered alongside the built-ins.
227
+ * Each value is a `PresetConfigSlice` (a `ChaosConfig` minus `presets`,
228
+ * `customPresets`, `seed`, and `debug`). Names collide fail-fast against
229
+ * built-ins and against each other — pick a unique label.
230
+ *
231
+ * Custom preset literals stay mutable on input; the engine deep-clones them
232
+ * during expansion, so post-construction tweaks are not observed by the
233
+ * runtime.
234
+ */
235
+ customPresets?: Record<string, PresetConfigSlice>;
236
+ /**
237
+ * Reserved for forward-compatibility with future shape changes.
238
+ * Defaults to `1`. Unknown values are rejected at validation time with
239
+ * `code: 'unknown_schema_version'`. Omit this field unless a future major
240
+ * release explicitly bumps the supported version.
241
+ */
242
+ schemaVersion?: 1;
175
243
  /**
176
244
  * Seed for Chaos Maker's PRNG.
177
245
  *
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Debug Mode.
3
+ *
4
+ * Two-sink logger that fires when `ChaosConfig.debug` is `true`:
5
+ * 1. Structured `type: 'debug'` events through `ChaosEventEmitter` —
6
+ * consumers subscribe via `instance.on('debug', cb)` and switch on
7
+ * `event.detail.stage` for the stage taxonomy.
8
+ * 2. A formatted `[Chaos] <stage> ...` line (or `[Chaos SW] <stage> ...`
9
+ * when the logger targets a Service Worker) to `console.debug`. Hidden
10
+ * by default in CI loggers, visible in browser DevTools.
11
+ *
12
+ * Framework-agnostic: never reads `process.env.DEBUG`, `--debug`, or
13
+ * `localStorage.debug`. The only signal is `ChaosConfig.debug`.
14
+ */
15
+ import type { ChaosConfig } from './config';
16
+ import type { ChaosDebugStage, ChaosEvent } from './events';
17
+ export type { ChaosDebugStage } from './events';
18
+ export interface DebugOptions {
19
+ enabled: boolean;
20
+ }
21
+ export declare function normalizeDebugOption(input: boolean | DebugOptions | undefined): DebugOptions;
22
+ /** Identity assigned to every rule object in a config snapshot. */
23
+ export interface RuleIdEntry {
24
+ ruleType: string;
25
+ ruleId: string;
26
+ }
27
+ /**
28
+ * Build a positional rule-id map for a config snapshot. IDs are
29
+ * `<ruleType>#<index>` derived from the order rules appear in their array.
30
+ * Reordering rules between runs changes the IDs — acceptable for in-test
31
+ * diagnostics.
32
+ */
33
+ export declare function buildRuleIdMap(config: ChaosConfig): WeakMap<object, RuleIdEntry>;
34
+ /**
35
+ * Build the human-readable body mirrored to `console.debug`. Does NOT include
36
+ * the `[Chaos]` / `[Chaos SW]` prefix — that is owned by `Logger.log()` and
37
+ * varies by target so the two never compose into a doubled prefix.
38
+ */
39
+ export declare function formatDebugMessage(stage: ChaosDebugStage, detail: ChaosEvent['detail']): string;
40
+ export declare class Logger {
41
+ private readonly opts;
42
+ private readonly target;
43
+ constructor(opts: DebugOptions, target?: 'page' | 'sw');
44
+ isEnabled(): boolean;
45
+ /**
46
+ * Build a `type: 'debug'` event with `detail.stage = stage`, mirror a
47
+ * `[Chaos] ...` (page) or `[Chaos SW] ...` (Service Worker) line to
48
+ * `console.debug`, and return the event for the emitter to fan out. The
49
+ * formatted string is never stored on the event payload.
50
+ *
51
+ * Returns `null` when the logger was constructed with `enabled: false`.
52
+ * Internal callers (the emitter fast-path) never reach this branch because
53
+ * `ChaosMaker` does not attach a logger when debug is off, but the guard
54
+ * keeps the public `Logger` API consistent with the `DebugOptions.enabled`
55
+ * contract for external consumers who instantiate it directly.
56
+ */
57
+ log(stage: ChaosDebugStage, detail: ChaosEvent['detail']): ChaosEvent | null;
58
+ }
@@ -1,4 +1,16 @@
1
+ import type { ValidationIssue } from './validation-types';
2
+ /** Aggregate validation failure thrown by `validateChaosConfig` /
3
+ * `prepareChaosConfig` / `validateConfig`.
4
+ *
5
+ * Issues are deterministically sorted (by `path` then `code`) before render
6
+ * so CI logs and snapshot tests stay stable across runs. The render is
7
+ * capped at 50 entries with a `... and N more` summary; the full list is
8
+ * always retained on `.issues` for programmatic inspection. */
1
9
  export declare class ChaosConfigError extends Error {
2
- readonly issues: string[];
3
- constructor(issues: string[]);
10
+ readonly issues: ValidationIssue[];
11
+ constructor(input: ValidationIssue[] | string[]);
12
+ /** v0.4.x-shaped string array. Concatenates `path` + `message` per issue
13
+ * in the same sorted order as `.issues`. Use for log greps that already
14
+ * consume the legacy shape; new code should read `.issues` directly. */
15
+ get messages(): string[];
4
16
  }
@@ -1,4 +1,25 @@
1
- export type ChaosEventType = 'network:failure' | 'network:latency' | 'network:abort' | 'network:corruption' | 'network:cors' | 'ui:assault' | 'websocket:drop' | 'websocket:delay' | 'websocket:corrupt' | 'websocket:close' | 'sse:drop' | 'sse:delay' | 'sse:corrupt' | 'sse:close';
1
+ import type { Logger, RuleIdEntry } from './debug';
2
+ export type ChaosEventType = 'network:failure' | 'network:latency' | 'network:abort' | 'network:corruption' | 'network:cors' | 'ui:assault' | 'websocket:drop' | 'websocket:delay' | 'websocket:corrupt' | 'websocket:close' | 'sse:drop' | 'sse:delay' | 'sse:corrupt' | 'sse:close'
3
+ /** Emitted once per `enableGroup()` call. `applied: true`. */
4
+ | 'rule-group:enabled'
5
+ /** Emitted once per `disableGroup()` call. `applied: true`. */
6
+ | 'rule-group:disabled'
7
+ /** Emitted once per group per toggle cycle when a rule is skipped because
8
+ * its group is disabled. Deduped — at most one event per group between
9
+ * toggles to avoid log floods. `applied: false`. */
10
+ | 'rule-group:gated'
11
+ /** Single Debug Mode event type. The concrete stage of the rule
12
+ * decision pipeline lives on `detail.stage`. `applied: false`. */
13
+ | 'debug';
14
+ /** Stage taxonomy. Stable strings used as `detail.stage` on every
15
+ * `type: 'debug'` event. Defined here (not in `debug.ts`) so the event-detail
16
+ * union can reference it without a circular runtime import. */
17
+ export type ChaosDebugStage = 'rule-evaluating' | 'rule-matched' | 'rule-skip-match' | 'rule-skip-counting' | 'rule-skip-group' | 'rule-skip-probability' | 'rule-applied' | 'lifecycle';
18
+ /** Lifecycle phases. Set on `detail.phase` only when
19
+ * `detail.stage === 'lifecycle'`. WS/SSE direction continues to live on
20
+ * the existing `detail.direction` field — `phase` is intentionally
21
+ * lifecycle-only to avoid overloading. */
22
+ export type ChaosLifecyclePhase = 'engine:start' | 'engine:stop' | 'engine:group-toggled' | 'sw:install' | 'sw:config-applied' | 'sw:config-stopped' | 'sw:group-toggled';
2
23
  export interface ChaosEvent {
3
24
  type: ChaosEventType;
4
25
  timestamp: number;
@@ -28,6 +49,29 @@ export interface ChaosEvent {
28
49
  operationName?: string;
29
50
  /** Reason string for diagnostic `applied: false` events. */
30
51
  reason?: string;
52
+ /** Group name (for `rule-group:*` events, and on gated rule diagnostics). */
53
+ groupName?: string;
54
+ /** New state of a group when `stage === 'lifecycle'` and
55
+ * `phase === 'engine:group-toggled' | 'sw:group-toggled'`. Distinguishes
56
+ * enable from disable on the debug stream so consumers don't have to
57
+ * pivot on the parallel `rule-group:enabled` / `rule-group:disabled`
58
+ * emitter events. */
59
+ enabled?: boolean;
60
+ /** Concrete stage of a rule's decision pipeline. Set on every
61
+ * `type: 'debug'` event; unset on non-debug events. */
62
+ stage?: ChaosDebugStage;
63
+ /** Lifecycle phase, set only when `stage === 'lifecycle'`. */
64
+ phase?: ChaosLifecyclePhase;
65
+ /** Rule category — `'failure' | 'latency' | 'abort' | ...`. */
66
+ ruleType?: string;
67
+ /** Deterministic identifier for a specific rule WITHIN A SINGLE
68
+ * CONFIG SNAPSHOT. Positional: reordering rules in your config changes
69
+ * the IDs. Sufficient for in-test diagnostic pinpointing in v0.5.0. */
70
+ ruleId?: string;
71
+ /** Optional human label for a rule (future builder field).
72
+ * Reserved so the event shape doesn't churn when the builder later
73
+ * gains `.failRequests({..., name: 'slow-api'})`. */
74
+ ruleName?: string;
31
75
  };
32
76
  }
33
77
  export type ChaosEventListener = (event: ChaosEvent) => void;
@@ -35,10 +79,24 @@ export declare class ChaosEventEmitter {
35
79
  private readonly maxLogEntries;
36
80
  private listeners;
37
81
  private log;
82
+ private logger;
83
+ private ruleIds;
38
84
  constructor(maxLogEntries?: number);
39
85
  on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
40
86
  off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
41
87
  emit(event: ChaosEvent): void;
88
+ /** Attach a Debug Mode logger. When unset, `debug()` is a fast-path no-op. */
89
+ setLogger(logger: Logger | undefined): void;
90
+ /** Attach the rule-id map so debug events auto-resolve `ruleType` /
91
+ * `ruleId` from a rule object reference. */
92
+ setRuleIds(map: WeakMap<object, RuleIdEntry> | undefined): void;
93
+ /**
94
+ * Emit a Debug Mode event. Fast-path no-op when no logger is attached —
95
+ * single undefined-check before any allocation. When `rule` is supplied
96
+ * and present in the rule-id map, `detail.ruleType` and `detail.ruleId`
97
+ * are filled in automatically.
98
+ */
99
+ debug(stage: ChaosDebugStage, detail: ChaosEvent['detail'], rule?: object): void;
42
100
  getLog(): ChaosEvent[];
43
101
  clearLog(): void;
44
102
  private notify;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Rule Groups — bulk runtime enable/disable for chaos rules.
3
+ *
4
+ * A `RuleGroupRegistry` tracks named groups (default-on) and answers
5
+ * `isActive(name)` from interceptors before they apply chaos. Groups not
6
+ * declared up-front are auto-registered the first time they are referenced
7
+ * (typo surfacing — appears in `list()` / `getSnapshot()`).
8
+ *
9
+ * Toggle is runtime-only: there is no engine restart, so `requestCounters`
10
+ * survive across `setEnabled()` calls.
11
+ */
12
+ /** Name of the implicit group all rules without an explicit `group` belong to. */
13
+ export declare const DEFAULT_GROUP_NAME = "default";
14
+ /** Public, declarative form accepted on `ChaosConfig.groups`. */
15
+ export interface RuleGroupConfig {
16
+ name: string;
17
+ enabled?: boolean;
18
+ }
19
+ /** Snapshot record describing a group inside the registry. */
20
+ export interface RuleGroup {
21
+ readonly name: string;
22
+ enabled: boolean;
23
+ /** True when registered via `ChaosConfig.groups` or `createGroup()`. False when auto-registered from `isActive()`. */
24
+ explicit: boolean;
25
+ }
26
+ export declare class RuleGroupRegistry {
27
+ private groups;
28
+ /**
29
+ * Tracks groups that have already emitted a `rule-group:gated` event since
30
+ * the last toggle. Cleared on every `setEnabled()` so the next toggle cycle
31
+ * gets a fresh diagnostic event without flooding.
32
+ */
33
+ private gatedEmitted;
34
+ /**
35
+ * Look up an existing group or create a new one.
36
+ *
37
+ * - Implicit (auto-create from `isActive`): `explicit: false`, defaults
38
+ * `enabled: true`.
39
+ * - Explicit (`ChaosConfig.groups` / `createGroup()`): `explicit: true`.
40
+ * When called with both `explicit: true` and `enabled` set, the existing
41
+ * group's `enabled` is overwritten; the explicit form is the source of truth.
42
+ */
43
+ ensure(name: string, opts?: {
44
+ enabled?: boolean;
45
+ explicit?: boolean;
46
+ }): RuleGroup;
47
+ setEnabled(name: string, enabled: boolean): void;
48
+ /**
49
+ * Auto-creates unknown groups on first check (implicit). Rationale:
50
+ * silently returning `true` for unknown names lets typos like
51
+ * `group: 'paymets'` mask chaos as if no group existed; auto-registering
52
+ * surfaces the typo via `list()` / `getSnapshot()` and keeps default-on
53
+ * backward compat.
54
+ */
55
+ isActive(name: string | undefined): boolean;
56
+ /** True when this group should emit a `rule-group:gated` event right now (first block since last toggle). */
57
+ shouldEmitGated(name: string): boolean;
58
+ has(name: string): boolean;
59
+ /**
60
+ * Remove a group from the registry.
61
+ * - `'default'` cannot be removed (returns `false`).
62
+ * - By default, throws when any rule in `referencedBy` still uses the group.
63
+ * - Pass `{ force: true }` to remove anyway. Subsequent `isActive(name)`
64
+ * calls auto-recreate the group (default-on).
65
+ */
66
+ remove(name: string, referencedBy: ReadonlySet<string>, opts?: {
67
+ force?: boolean;
68
+ }): boolean;
69
+ list(): RuleGroup[];
70
+ getSnapshot(): Record<string, boolean>;
71
+ }
@@ -1,14 +1,39 @@
1
1
  import { ChaosMaker } from './ChaosMaker';
2
- import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher } from './config';
2
+ import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, RuleGroupAssignment, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher } from './config';
3
3
  import { ChaosConfigError } from './errors';
4
- import { validateConfig } from './validation';
4
+ import { validateConfig, prepareChaosConfig, validateChaosConfig, VALIDATOR_BRAND_VERSION, type ValidateChaosConfigOptions, type PrepareChaosConfigOptions } from './validation';
5
5
  import { ChaosEvent, ChaosEventType, ChaosEventListener, ChaosEventEmitter } from './events';
6
6
  import { ChaosConfigBuilder } from './builder';
7
- import { presets } from './presets';
7
+ import { presets, PresetRegistry, BUILT_IN_PRESETS, expandPresets } from './presets';
8
8
  import { createPrng, generateSeed } from './prng';
9
- export { ChaosMaker, ChaosConfigError, validateConfig, ChaosEventEmitter, ChaosConfigBuilder, presets, createPrng, generateSeed };
9
+ /** `validateChaosConfig` is the canonical structured validation entry. Layers
10
+ * schema-version gating, brand-cache short-circuit, deprecation walk, and
11
+ * custom validators on top of `prepareChaosConfig` (Zod pass 1 + preset
12
+ * expansion + Zod pass 2). The `ChaosMaker` constructor and every adapter
13
+ * call through it.
14
+ *
15
+ * `prepareChaosConfig` is the lower-level primitive without the brand /
16
+ * deprecation / customValidators layers; useful in advanced flows that
17
+ * manage their own re-validation cadence.
18
+ *
19
+ * `validateConfig` is the schema-only primitive — does NOT expand presets.
20
+ * Use only for unit-test structural assertions. */
21
+ export { ChaosMaker, ChaosConfigError, validateConfig, prepareChaosConfig, validateChaosConfig, VALIDATOR_BRAND_VERSION, ChaosEventEmitter, ChaosConfigBuilder, presets, PresetRegistry, BUILT_IN_PRESETS, expandPresets, createPrng, generateSeed };
22
+ /** Internal: prebuilt Zod schema variants. Exported so the JSON-schema build
23
+ * script can serialize the canonical strict variant. Application code should
24
+ * call `validateChaosConfig` instead — the schemas are not the public
25
+ * validation surface and may evolve without a version bump. */
26
+ export { chaosConfigSchemaStrict, chaosConfigSchemaPassthrough } from './validation';
27
+ export type { ValidateChaosConfigOptions, PrepareChaosConfigOptions };
28
+ export type { ValidationIssue, ValidationIssueCode, RuleType, CustomRuleValidator, CustomValidatorMap, DeprecationEntry } from './validation-types';
29
+ export type { Preset, PresetConfigSlice } from './presets';
10
30
  export { SW_BRIDGE_SOURCE } from './sw-bridge-source';
11
31
  export { extractGraphQLOperation, parseOperationFromQueryString, operationNameMatches } from './graphql';
12
32
  export { serializeForTransport, deserializeForTransport } from './transport';
33
+ export { DEFAULT_GROUP_NAME, RuleGroupRegistry } from './groups';
34
+ export { Logger, normalizeDebugOption, formatDebugMessage, buildRuleIdMap } from './debug';
35
+ export type { RuleGroup, RuleGroupConfig } from './groups';
13
36
  export type { GraphQLExtractResult, GraphQLRuleOutcome } from './graphql';
14
- export type { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher, ChaosEvent, ChaosEventType, ChaosEventListener };
37
+ export type { DebugOptions, ChaosDebugStage, RuleIdEntry } from './debug';
38
+ export type { ChaosLifecyclePhase } from './events';
39
+ export type { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, NetworkFailureConfig, NetworkLatencyConfig, NetworkAbortConfig, NetworkCorruptionConfig, NetworkCorsConfig, NetworkConfig, NetworkRuleMatchers, RuleGroupAssignment, UiAssaultConfig, UiConfig, WebSocketConfig, WebSocketDropConfig, WebSocketDelayConfig, WebSocketCorruptConfig, WebSocketCloseConfig, WebSocketDirection, WebSocketCorruptionStrategy, SSEConfig, SSEDropConfig, SSEDelayConfig, SSECorruptConfig, SSECloseConfig, SSECorruptionStrategy, SSEEventTypeMatcher, ChaosEvent, ChaosEventType, ChaosEventListener };
@@ -1,3 +1,4 @@
1
1
  import { UiConfig } from '../config';
2
2
  import { ChaosEventEmitter } from '../events';
3
- export declare function attachDomAssailant(config: UiConfig, random: () => number, emitter?: ChaosEventEmitter): MutationObserver;
3
+ import type { RuleGroupRegistry } from '../groups';
4
+ export declare function attachDomAssailant(config: UiConfig, random: () => number, emitter?: ChaosEventEmitter, groups?: RuleGroupRegistry): MutationObserver;
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { SSEConfig } from '../config';
27
27
  import { ChaosEventEmitter } from '../events';
28
+ import type { RuleGroupRegistry } from '../groups';
28
29
  export interface EventSourceLikeStatic {
29
30
  readonly CONNECTING: 0;
30
31
  readonly OPEN: 1;
@@ -38,4 +39,4 @@ export interface EventSourcePatchHandle {
38
39
  /** Cancel pending timers and disarm wrapped instances. Call on ChaosMaker.stop(). */
39
40
  uninstall(): void;
40
41
  }
41
- export declare function patchEventSource(OriginalEventSource: EventSourceLikeStatic, config: SSEConfig, emitter: ChaosEventEmitter, random: () => number, counters: Map<object, number>): EventSourcePatchHandle;
42
+ export declare function patchEventSource(OriginalEventSource: EventSourceLikeStatic, config: SSEConfig, emitter: ChaosEventEmitter, random: () => number, counters: Map<object, number>, groups?: RuleGroupRegistry): EventSourcePatchHandle;
@@ -1,3 +1,4 @@
1
1
  import { NetworkConfig } from '../config';
2
+ import type { RuleGroupRegistry } from '../groups';
2
3
  import { ChaosEventEmitter } from '../events';
3
- export declare function patchFetch(originalFetch: typeof globalThis.fetch, config: NetworkConfig, random: () => number, emitter?: ChaosEventEmitter, counters?: Map<object, number>): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
4
+ export declare function patchFetch(originalFetch: typeof globalThis.fetch, config: NetworkConfig, random: () => number, emitter?: ChaosEventEmitter, counters?: Map<object, number>, groups?: RuleGroupRegistry): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
@@ -1,4 +1,5 @@
1
1
  import { NetworkConfig } from '../config';
2
+ import type { RuleGroupRegistry } from '../groups';
2
3
  import { ChaosEventEmitter } from '../events';
3
- export declare function patchXHR(originalXhrSend: (body?: Document | XMLHttpRequestBodyInit) => void, config: NetworkConfig, random: () => number, emitter?: ChaosEventEmitter, counters?: Map<object, number>): (this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit) => void;
4
+ export declare function patchXHR(originalXhrSend: (body?: Document | XMLHttpRequestBodyInit) => void, config: NetworkConfig, random: () => number, emitter?: ChaosEventEmitter, counters?: Map<object, number>, groups?: RuleGroupRegistry): (this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit) => void;
4
5
  export declare function patchXHROpen(originalXhrOpen: (method: string, url: string | URL) => void): (this: XMLHttpRequest, method: string, url: string | URL) => void;
@@ -21,10 +21,11 @@
21
21
  */
22
22
  import { WebSocketConfig } from '../config';
23
23
  import { ChaosEventEmitter } from '../events';
24
+ import type { RuleGroupRegistry } from '../groups';
24
25
  export interface WebSocketPatchHandle {
25
26
  /** Wrapped WebSocket constructor suitable for `globalThis.WebSocket = …`. */
26
27
  readonly Wrapped: typeof WebSocket;
27
28
  /** Clear all pending delay timers and emit drop events for them. Call on ChaosMaker.stop(). */
28
29
  uninstall(): void;
29
30
  }
30
- export declare function patchWebSocket(OriginalWebSocket: typeof WebSocket, config: WebSocketConfig, emitter: ChaosEventEmitter, random: () => number, counters: Map<object, number>): WebSocketPatchHandle;
31
+ export declare function patchWebSocket(OriginalWebSocket: typeof WebSocket, config: WebSocketConfig, emitter: ChaosEventEmitter, random: () => number, counters: Map<object, number>, groups?: RuleGroupRegistry): WebSocketPatchHandle;