@scenarist/core 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. package/dist/adapters/console-logger.d.ts +40 -0
  2. package/dist/adapters/console-logger.d.ts.map +1 -0
  3. package/dist/adapters/console-logger.js +193 -0
  4. package/dist/adapters/in-memory-state-manager.d.ts.map +1 -1
  5. package/dist/adapters/in-memory-state-manager.js +3 -0
  6. package/dist/adapters/index.d.ts +2 -0
  7. package/dist/adapters/index.d.ts.map +1 -1
  8. package/dist/adapters/index.js +2 -0
  9. package/dist/adapters/noop-logger.d.ts +26 -0
  10. package/dist/adapters/noop-logger.d.ts.map +1 -0
  11. package/dist/adapters/noop-logger.js +26 -0
  12. package/dist/contracts/framework-adapter.d.ts +18 -0
  13. package/dist/contracts/framework-adapter.d.ts.map +1 -1
  14. package/dist/domain/config-builder.d.ts.map +1 -1
  15. package/dist/domain/config-builder.js +13 -0
  16. package/dist/domain/deep-equals.d.ts.map +1 -1
  17. package/dist/domain/deep-equals.js +2 -0
  18. package/dist/domain/index.d.ts +1 -0
  19. package/dist/domain/index.d.ts.map +1 -1
  20. package/dist/domain/index.js +1 -0
  21. package/dist/domain/log-events.d.ts +67 -0
  22. package/dist/domain/log-events.d.ts.map +1 -0
  23. package/dist/domain/log-events.js +70 -0
  24. package/dist/domain/path-extraction.js +1 -0
  25. package/dist/domain/regex-matching.d.ts.map +1 -1
  26. package/dist/domain/regex-matching.js +1 -0
  27. package/dist/domain/response-selector.d.ts +2 -1
  28. package/dist/domain/response-selector.d.ts.map +1 -1
  29. package/dist/domain/response-selector.js +93 -11
  30. package/dist/domain/scenario-manager.d.ts +4 -2
  31. package/dist/domain/scenario-manager.d.ts.map +1 -1
  32. package/dist/domain/scenario-manager.js +42 -24
  33. package/dist/domain/state-condition-evaluator.js +1 -0
  34. package/dist/domain/template-replacement.d.ts.map +1 -1
  35. package/dist/domain/template-replacement.js +3 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/ports/driven/logger.d.ts +119 -0
  40. package/dist/ports/driven/logger.d.ts.map +1 -0
  41. package/dist/ports/driven/logger.js +1 -0
  42. package/dist/ports/driven/response-selector.d.ts +3 -1
  43. package/dist/ports/driven/response-selector.d.ts.map +1 -1
  44. package/dist/ports/driven/response-selector.js +1 -0
  45. package/dist/ports/index.d.ts +1 -0
  46. package/dist/ports/index.d.ts.map +1 -1
  47. package/dist/types/config.d.ts +29 -0
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/types/errors.d.ts +49 -0
  50. package/dist/types/errors.d.ts.map +1 -0
  51. package/dist/types/errors.js +26 -0
  52. package/dist/types/index.d.ts +3 -1
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/dist/types/index.js +2 -1
  55. package/package.json +5 -5
@@ -1,9 +1,11 @@
1
- import { ResponseSelectionError } from "../ports/driven/response-selector.js";
1
+ import { ScenaristError, ErrorCodes } from "../types/errors.js";
2
2
  import { extractFromPath } from "./path-extraction.js";
3
3
  import { applyTemplates } from "./template-replacement.js";
4
4
  import { matchesRegex } from "./regex-matching.js";
5
5
  import { createStateResponseResolver } from "./state-response-resolver.js";
6
6
  import { deepEquals } from "./deep-equals.js";
7
+ import { noOpLogger } from "../adapters/index.js";
8
+ import { LogCategories, LogEvents } from "./log-events.js";
7
9
  const SPECIFICITY_RANGES = {
8
10
  MATCH_CRITERIA_BASE: 100,
9
11
  SEQUENCE_FALLBACK: 1,
@@ -21,26 +23,41 @@ const SPECIFICITY_RANGES = {
21
23
  * @param options.stateManager - Optional state manager for capture/injection (Phase 3)
22
24
  */
23
25
  export const createResponseSelector = (options = {}) => {
24
- const { sequenceTracker, stateManager } = options;
26
+ const { sequenceTracker, stateManager, logger = noOpLogger } = options;
25
27
  return {
26
28
  selectResponse(testId, scenarioId, context, mocks) {
29
+ const logContext = { testId, scenarioId };
30
+ // Log the number of candidate mocks
31
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_CANDIDATES_FOUND, logContext, {
32
+ count: mocks.length,
33
+ });
27
34
  let bestMatch = null;
35
+ // Track if we skipped any exhausted sequences (for better error messages)
36
+ let skippedExhaustedSequences = false;
28
37
  // Find all matching mocks and score them by specificity
29
38
  for (let mockIndex = 0; mockIndex < mocks.length; mockIndex++) {
30
- // Index is guaranteed in bounds by loop condition (0 <= mockIndex < length)
39
+ // eslint-disable-next-line security/detect-object-injection -- Index bounded by loop (0 <= i < length)
31
40
  const mockWithParams = mocks[mockIndex];
32
41
  const mock = mockWithParams.mock;
33
42
  // Skip exhausted sequences (repeat: 'none' that have been exhausted)
34
43
  if (mock.sequence && sequenceTracker) {
35
44
  const { exhausted } = sequenceTracker.getPosition(testId, scenarioId, mockIndex);
36
45
  if (exhausted) {
46
+ skippedExhaustedSequences = true;
37
47
  continue; // Skip to next mock, allowing fallback to be selected
38
48
  }
39
49
  }
40
50
  // Check if this mock has match criteria
41
51
  if (mock.match) {
42
52
  // If match criteria exists, check if it matches the request
43
- if (matchesCriteria(context, mock.match, testId, stateManager)) {
53
+ const matched = matchesCriteria(context, mock.match, testId, stateManager);
54
+ // Log the evaluation result
55
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
56
+ mockIndex,
57
+ matched,
58
+ hasCriteria: true,
59
+ });
60
+ if (matched) {
44
61
  // Match criteria always have higher priority than fallbacks
45
62
  // Base specificity ensures even 1 field beats any fallback
46
63
  const specificity = SPECIFICITY_RANGES.MATCH_CRITERIA_BASE +
@@ -55,9 +72,16 @@ export const createResponseSelector = (options = {}) => {
55
72
  continue;
56
73
  }
57
74
  // No match criteria = fallback mock (always matches)
58
- // Sequences get higher priority than simple responses
59
- // This ensures sequences are selected over simple fallback responses
60
- const fallbackSpecificity = mock.sequence
75
+ // Log fallback evaluation
76
+ logger.debug(LogCategories.MATCHING, LogEvents.MOCK_MATCH_EVALUATED, logContext, {
77
+ mockIndex,
78
+ matched: true,
79
+ hasCriteria: false,
80
+ });
81
+ // Dynamic response types (sequence, stateResponse) get higher priority than simple responses
82
+ // This ensures they are selected over simple fallback responses
83
+ // Both sequence and stateResponse get the same specificity (Issue #316 fix)
84
+ const fallbackSpecificity = mock.sequence || mock.stateResponse
61
85
  ? SPECIFICITY_RANGES.SEQUENCE_FALLBACK
62
86
  : SPECIFICITY_RANGES.SIMPLE_FALLBACK;
63
87
  if (!bestMatch || fallbackSpecificity >= bestMatch.specificity) {
@@ -73,14 +97,29 @@ export const createResponseSelector = (options = {}) => {
73
97
  }
74
98
  // Return the best matching mock
75
99
  if (bestMatch) {
76
- const { mockWithParams, mockIndex } = bestMatch;
100
+ const { mockWithParams, mockIndex, specificity } = bestMatch;
77
101
  const mock = mockWithParams.mock;
102
+ // Log successful selection
103
+ logger.info(LogCategories.MATCHING, LogEvents.MOCK_SELECTED, logContext, {
104
+ mockIndex,
105
+ specificity,
106
+ });
78
107
  // Select response (single, sequence, or stateResponse)
79
108
  const response = selectResponseFromMock(testId, scenarioId, mockIndex, mock, sequenceTracker, stateManager);
80
109
  if (!response) {
81
110
  return {
82
111
  success: false,
83
- error: new ResponseSelectionError(`Mock has neither response nor sequence field`),
112
+ error: new ScenaristError(`Mock has neither response nor sequence field`, {
113
+ code: ErrorCodes.VALIDATION_ERROR,
114
+ context: {
115
+ testId,
116
+ scenarioId,
117
+ mockInfo: {
118
+ index: mockIndex,
119
+ },
120
+ hint: "Each mock must have a 'response', 'sequence', or 'stateResponse' field.",
121
+ },
122
+ }),
84
123
  };
85
124
  }
86
125
  // Phase 3: Capture state from request if configured
@@ -105,10 +144,48 @@ export const createResponseSelector = (options = {}) => {
105
144
  }
106
145
  return { success: true, data: finalResponse };
107
146
  }
108
- // No mock matched
147
+ // No mock matched - determine specific error type
148
+ if (skippedExhaustedSequences) {
149
+ // All matching mocks were exhausted sequences
150
+ logger.warn(LogCategories.SEQUENCE, LogEvents.SEQUENCE_EXHAUSTED, logContext, {
151
+ url: context.url,
152
+ method: context.method,
153
+ });
154
+ return {
155
+ success: false,
156
+ error: new ScenaristError(`Sequence exhausted for ${context.method} ${context.url}. All responses have been consumed and repeat mode is 'none'.`, {
157
+ code: ErrorCodes.SEQUENCE_EXHAUSTED,
158
+ context: {
159
+ testId,
160
+ scenarioId,
161
+ requestInfo: {
162
+ method: context.method,
163
+ url: context.url,
164
+ },
165
+ hint: "Add a fallback mock to handle requests after sequence exhaustion, or use repeat: 'last' or 'cycle' instead of 'none'.",
166
+ },
167
+ }),
168
+ };
169
+ }
170
+ logger.warn(LogCategories.MATCHING, LogEvents.MOCK_NO_MATCH, logContext, {
171
+ url: context.url,
172
+ method: context.method,
173
+ candidateCount: 0,
174
+ });
109
175
  return {
110
176
  success: false,
111
- error: new ResponseSelectionError(`No mock matched for ${context.method} ${context.url}`),
177
+ error: new ScenaristError(`No mock matched for ${context.method} ${context.url}`, {
178
+ code: ErrorCodes.NO_MOCK_FOUND,
179
+ context: {
180
+ testId,
181
+ scenarioId,
182
+ requestInfo: {
183
+ method: context.method,
184
+ url: context.url,
185
+ },
186
+ hint: "Add a fallback mock (without match criteria) to handle unmatched requests, or add a mock with matching criteria.",
187
+ },
188
+ }),
112
189
  };
113
190
  },
114
191
  };
@@ -136,6 +213,7 @@ const selectResponseFromMock = (testId, scenarioId, mockIndex, mock, sequenceTra
136
213
  // Get response at current position
137
214
  // Note: Exhausted sequences are skipped during matching phase,
138
215
  // so position should always be valid here
216
+ // eslint-disable-next-line security/detect-object-injection -- Position bounded by sequence tracker
139
217
  const response = mock.sequence.responses[position];
140
218
  // Advance position for next call
141
219
  const repeatMode = mock.sequence.repeat || "last";
@@ -275,6 +353,7 @@ const matchesState = (stateCriteria, testId, stateManager) => {
275
353
  return false;
276
354
  }
277
355
  // Deep equality check for values (handles primitives, null, objects)
356
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
278
357
  if (!deepEquals(currentState[key], expectedValue)) {
279
358
  return false;
280
359
  }
@@ -295,6 +374,7 @@ const matchesBody = (requestBody, criteriaBody) => {
295
374
  const body = requestBody;
296
375
  // Check all required fields exist in request body with matching values
297
376
  for (const [key, criteriaValue] of Object.entries(criteriaBody)) {
377
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
298
378
  const requestValue = body[key];
299
379
  // Convert to string for matching (type coercion like headers/query)
300
380
  const stringValue = requestValue == null ? "" : String(requestValue);
@@ -377,6 +457,7 @@ const matchesHeaders = (requestHeaders, criteriaHeaders) => {
377
457
  const normalizedRequest = createNormalizedHeaderMap(requestHeaders);
378
458
  for (const [key, value] of Object.entries(criteriaHeaders)) {
379
459
  const normalizedKey = normalizeHeaderName(key);
460
+ // eslint-disable-next-line security/detect-object-injection -- Key normalized from Object.entries iteration
380
461
  const requestValue = normalizedRequest[normalizedKey];
381
462
  if (!requestValue || !matchesValue(requestValue, value)) {
382
463
  return false;
@@ -391,6 +472,7 @@ const matchesHeaders = (requestHeaders, criteriaHeaders) => {
391
472
  const matchesQuery = (requestQuery, criteriaQuery) => {
392
473
  // Check all required query params exist with exact matching values
393
474
  for (const [key, value] of Object.entries(criteriaQuery)) {
475
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
394
476
  const requestValue = requestQuery[key];
395
477
  if (!requestValue || !matchesValue(requestValue, value)) {
396
478
  return false;
@@ -1,4 +1,4 @@
1
- import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker, StateManager } from "../ports/index.js";
1
+ import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker, StateManager, Logger } from "../ports/index.js";
2
2
  /**
3
3
  * Factory function to create a ScenarioManager implementation.
4
4
  *
@@ -8,13 +8,15 @@ import type { ScenarioManager, ScenarioRegistry, ScenarioStore, SequenceTracker,
8
8
  * - Any registry implementation (in-memory, Redis, files, remote)
9
9
  * - Any store implementation (in-memory, Redis, database)
10
10
  * - Any state manager implementation (in-memory, Redis, database)
11
+ * - Any logger implementation (console, file, remote)
11
12
  * - Proper testing with mock dependencies
12
13
  * - True hexagonal architecture
13
14
  */
14
- export declare const createScenarioManager: ({ registry, store, stateManager, sequenceTracker, }: {
15
+ export declare const createScenarioManager: ({ registry, store, stateManager, sequenceTracker, logger, }: {
15
16
  registry: ScenarioRegistry;
16
17
  store: ScenarioStore;
17
18
  stateManager?: StateManager;
18
19
  sequenceTracker?: SequenceTracker;
20
+ logger?: Logger;
19
21
  }) => ScenarioManager;
20
22
  //# sourceMappingURL=scenario-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACb,MAAM,mBAAmB,CAAC;AAkC3B;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GAAI,qDAKnC;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,KAAG,eAgFH,CAAC"}
1
+ {"version":3,"file":"scenario-manager.d.ts","sourceRoot":"","sources":["../../src/domain/scenario-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,YAAY,EACZ,MAAM,EACP,MAAM,mBAAmB,CAAC;AAwC3B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GAAI,6DAMnC;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,KAAG,eAwHH,CAAC"}
@@ -1,24 +1,25 @@
1
+ import { noOpLogger } from "../adapters/index.js";
1
2
  import { ScenaristScenarioSchema } from "../schemas/index.js";
2
- class ScenarioNotFoundError extends Error {
3
- constructor(scenarioId) {
4
- super(`Scenario '${scenarioId}' not found. Did you forget to register it?`);
5
- this.name = "ScenarioNotFoundError";
6
- }
7
- }
8
- class DuplicateScenarioError extends Error {
9
- constructor(scenarioId) {
10
- super(`Scenario '${scenarioId}' is already registered. Each scenario must have a unique ID.`);
11
- this.name = "DuplicateScenarioError";
12
- }
13
- }
14
- class ScenarioValidationError extends Error {
15
- validationErrors;
16
- constructor(message, validationErrors) {
17
- super(message);
18
- this.validationErrors = validationErrors;
19
- this.name = "ScenarioValidationError";
20
- }
21
- }
3
+ import { ScenaristError, ErrorCodes } from "../types/errors.js";
4
+ import { LogCategories, LogEvents } from "./log-events.js";
5
+ const createDuplicateScenarioError = (scenarioId) => {
6
+ return new ScenaristError(`Scenario '${scenarioId}' is already registered. Each scenario must have a unique ID.`, {
7
+ code: ErrorCodes.DUPLICATE_SCENARIO,
8
+ context: {
9
+ scenarioId,
10
+ hint: "Use a different scenario ID, or remove the existing scenario before registering a new one.",
11
+ },
12
+ });
13
+ };
14
+ const createScenarioValidationError = (scenarioId, validationErrors) => {
15
+ return new ScenaristError(`Invalid scenario definition for '${scenarioId}': ${validationErrors.join(", ")}`, {
16
+ code: ErrorCodes.VALIDATION_ERROR,
17
+ context: {
18
+ scenarioId,
19
+ hint: `Check your scenario definition. Validation errors: ${validationErrors.join("; ")}`,
20
+ },
21
+ });
22
+ };
22
23
  /**
23
24
  * Factory function to create a ScenarioManager implementation.
24
25
  *
@@ -28,10 +29,11 @@ class ScenarioValidationError extends Error {
28
29
  * - Any registry implementation (in-memory, Redis, files, remote)
29
30
  * - Any store implementation (in-memory, Redis, database)
30
31
  * - Any state manager implementation (in-memory, Redis, database)
32
+ * - Any logger implementation (console, file, remote)
31
33
  * - Proper testing with mock dependencies
32
34
  * - True hexagonal architecture
33
35
  */
34
- export const createScenarioManager = ({ registry, store, stateManager, sequenceTracker, }) => {
36
+ export const createScenarioManager = ({ registry, store, stateManager, sequenceTracker, logger = noOpLogger, }) => {
35
37
  return {
36
38
  registerScenario(definition) {
37
39
  // Validate scenario definition at trust boundary
@@ -39,7 +41,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
39
41
  if (!validationResult.success) {
40
42
  const errorMessages = validationResult.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
41
43
  const scenarioId = definition?.id || "<unknown>";
42
- throw new ScenarioValidationError(`Invalid scenario definition for '${scenarioId}': ${errorMessages.join(", ")}`, errorMessages);
44
+ throw createScenarioValidationError(scenarioId, errorMessages);
43
45
  }
44
46
  const existing = registry.get(definition.id);
45
47
  // Allow re-registering the exact same scenario object (idempotent)
@@ -48,16 +50,30 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
48
50
  }
49
51
  // Prevent registering a different scenario with the same ID
50
52
  if (existing) {
51
- throw new DuplicateScenarioError(definition.id);
53
+ throw createDuplicateScenarioError(definition.id);
52
54
  }
53
55
  registry.register(definition);
56
+ logger.debug(LogCategories.SCENARIO, LogEvents.SCENARIO_REGISTERED, {}, {
57
+ scenarioId: definition.id,
58
+ mockCount: definition.mocks.length, // mocks is required by ScenaristScenarioSchema
59
+ });
54
60
  },
55
61
  switchScenario(testId, scenarioId) {
56
62
  const definition = registry.get(scenarioId);
57
63
  if (!definition) {
64
+ logger.error(LogCategories.SCENARIO, LogEvents.SCENARIO_NOT_FOUND, { testId }, {
65
+ requestedScenarioId: scenarioId,
66
+ });
58
67
  return {
59
68
  success: false,
60
- error: new ScenarioNotFoundError(scenarioId),
69
+ error: new ScenaristError(`Scenario '${scenarioId}' not found. Did you forget to register it?`, {
70
+ code: ErrorCodes.SCENARIO_NOT_FOUND,
71
+ context: {
72
+ testId,
73
+ scenarioId,
74
+ hint: "Make sure to register the scenario before switching to it. Use manager.registerScenario(definition) first.",
75
+ },
76
+ }),
61
77
  };
62
78
  }
63
79
  const activeScenario = {
@@ -72,6 +88,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
72
88
  if (stateManager) {
73
89
  stateManager.reset(testId);
74
90
  }
91
+ logger.info(LogCategories.SCENARIO, LogEvents.SCENARIO_SWITCHED, { testId, scenarioId }, {});
75
92
  return { success: true, data: undefined };
76
93
  },
77
94
  getActiveScenario(testId) {
@@ -82,6 +99,7 @@ export const createScenarioManager = ({ registry, store, stateManager, sequenceT
82
99
  },
83
100
  clearScenario(testId) {
84
101
  store.delete(testId);
102
+ logger.debug(LogCategories.SCENARIO, LogEvents.SCENARIO_CLEARED, { testId }, {});
85
103
  },
86
104
  getScenarioById(id) {
87
105
  return registry.get(id);
@@ -32,6 +32,7 @@ const stateMatchesCondition = (state, when) => {
32
32
  if (!(key in state)) {
33
33
  return false;
34
34
  }
35
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
35
36
  const actualValue = state[key];
36
37
  if (!deepEquals(actualValue, expectedValue)) {
37
38
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,OAAO,EACd,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,OA8DF,CAAC"}
1
+ {"version":3,"file":"template-replacement.d.ts","sourceRoot":"","sources":["../../src/domain/template-replacement.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,OAAO,EACd,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,OA+DF,CAAC"}
@@ -49,6 +49,7 @@ export const applyTemplates = (value, templateData) => {
49
49
  if (typeof value === "object" && value !== null) {
50
50
  const result = {};
51
51
  for (const [key, val] of Object.entries(value)) {
52
+ // eslint-disable-next-line security/detect-object-injection -- Key from Object.entries iteration
52
53
  result[key] = applyTemplates(val, normalizedData);
53
54
  }
54
55
  return result;
@@ -67,6 +68,7 @@ export const applyTemplates = (value, templateData) => {
67
68
  */
68
69
  const resolveTemplatePath = (templateData, prefix, path) => {
69
70
  // Get the root object (state or params)
71
+ // eslint-disable-next-line security/detect-object-injection -- Prefix validated as 'state' or 'params' by regex
70
72
  const root = templateData[prefix];
71
73
  // Guard: Prefix doesn't exist (e.g., no params provided)
72
74
  if (root === undefined || typeof root !== "object" || root === null) {
@@ -86,6 +88,7 @@ const resolveTemplatePath = (templateData, prefix, path) => {
86
88
  }
87
89
  // Traverse object
88
90
  const record = current;
91
+ // eslint-disable-next-line security/detect-object-injection -- Segment from split() iteration
89
92
  current = record[segment];
90
93
  // Guard: Return undefined if property doesn't exist
91
94
  if (current === undefined) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type * from "./types/index.js";
2
+ export { ScenaristError, ErrorCodes } from "./types/errors.js";
2
3
  export * from "./schemas/index.js";
3
4
  export * from "./constants/index.js";
4
5
  export type * from "./ports/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,kBAAkB,CAAC;AAGtC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,sBAAsB,CAAC;AAGrC,mBAAmB,kBAAkB,CAAC;AAGtC,mBAAmB,sBAAsB,CAAC;AAG1C,cAAc,mBAAmB,CAAC;AAGlC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,kBAAkB,CAAC;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG/D,cAAc,oBAAoB,CAAC;AAGnC,cAAc,sBAAsB,CAAC;AAGrC,mBAAmB,kBAAkB,CAAC;AAGtC,mBAAmB,sBAAsB,CAAC;AAG1C,cAAc,mBAAmB,CAAC;AAGlC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // Error classes and constants (runtime values, not just types)
2
+ export { ScenaristError, ErrorCodes } from "./types/errors.js";
1
3
  // Schemas (runtime validation)
2
4
  export * from "./schemas/index.js";
3
5
  // Constants
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Log levels ordered by verbosity (ascending).
3
+ * Each level includes all less verbose levels.
4
+ *
5
+ * - silent: No output (production default)
6
+ * - error: Critical failures preventing operation
7
+ * - warn: Potential issues that may cause problems
8
+ * - info: Operation flow and key events
9
+ * - debug: Detailed decision logic
10
+ * - trace: Request/response bodies, verbose details
11
+ */
12
+ export type LogLevel = "silent" | "error" | "warn" | "info" | "debug" | "trace";
13
+ /**
14
+ * Log categories for filtering specific areas of concern.
15
+ * Users can enable/disable categories independently.
16
+ */
17
+ export type LogCategory = "lifecycle" | "scenario" | "matching" | "sequence" | "state" | "template" | "request";
18
+ /**
19
+ * Base context included in all log events.
20
+ * Enables filtering and correlation by test ID.
21
+ */
22
+ export type LogContext = {
23
+ readonly testId?: string;
24
+ readonly scenarioId?: string;
25
+ readonly requestUrl?: string;
26
+ readonly requestMethod?: string;
27
+ };
28
+ /**
29
+ * Structured log entry for capture and serialization.
30
+ */
31
+ export type LogEntry = {
32
+ readonly level: Exclude<LogLevel, "silent">;
33
+ readonly category: LogCategory;
34
+ readonly message: string;
35
+ readonly context: LogContext;
36
+ readonly data?: Record<string, unknown>;
37
+ readonly timestamp: number;
38
+ };
39
+ /**
40
+ * Logger port for structured logging.
41
+ *
42
+ * This is a driven (secondary) port - domain logic calls out to it,
43
+ * implementations are injected via dependency injection.
44
+ *
45
+ * Implementations must handle:
46
+ * - Level filtering (only emit logs at or above configured level)
47
+ * - Category filtering (optionally filter by category)
48
+ * - Output formatting (console, JSON, custom)
49
+ *
50
+ * This port enables:
51
+ * - NoOpLogger: Zero overhead when disabled (production, silent mode)
52
+ * - ConsoleLogger: Human-readable or JSON output (development)
53
+ * - TestLogger: Capture logs for assertion in tests
54
+ * - Custom loggers: User-provided implementations (Winston, Pino, etc.)
55
+ *
56
+ * All methods return void - logging is fire-and-forget.
57
+ * Implementations should never throw.
58
+ */
59
+ export interface Logger {
60
+ /**
61
+ * Log at error level - critical failures preventing operation.
62
+ *
63
+ * @param category - Area of concern for filtering
64
+ * @param message - Human-readable event description
65
+ * @param context - Request context for correlation (testId, scenarioId, etc.)
66
+ * @param data - Optional structured data for the event
67
+ */
68
+ error(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
69
+ /**
70
+ * Log at warn level - potential issues that may cause problems.
71
+ *
72
+ * @param category - Area of concern for filtering
73
+ * @param message - Human-readable event description
74
+ * @param context - Request context for correlation
75
+ * @param data - Optional structured data for the event
76
+ */
77
+ warn(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
78
+ /**
79
+ * Log at info level - operation flow and key events.
80
+ *
81
+ * @param category - Area of concern for filtering
82
+ * @param message - Human-readable event description
83
+ * @param context - Request context for correlation
84
+ * @param data - Optional structured data for the event
85
+ */
86
+ info(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
87
+ /**
88
+ * Log at debug level - detailed decision logic.
89
+ *
90
+ * @param category - Area of concern for filtering
91
+ * @param message - Human-readable event description
92
+ * @param context - Request context for correlation
93
+ * @param data - Optional structured data for the event
94
+ */
95
+ debug(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
96
+ /**
97
+ * Log at trace level - request/response bodies, verbose details.
98
+ *
99
+ * @param category - Area of concern for filtering
100
+ * @param message - Human-readable event description
101
+ * @param context - Request context for correlation
102
+ * @param data - Optional structured data for the event
103
+ */
104
+ trace(category: LogCategory, message: string, context: LogContext, data?: Record<string, unknown>): void;
105
+ /**
106
+ * Check if a specific level would be logged.
107
+ * Enables conditional expensive operations before logging.
108
+ *
109
+ * @example
110
+ * if (logger.isEnabled('trace')) {
111
+ * logger.trace('request', 'body', ctx, { body: JSON.stringify(large) });
112
+ * }
113
+ *
114
+ * @param level - The log level to check
115
+ * @returns true if logging at this level is enabled
116
+ */
117
+ isEnabled(level: Exclude<LogLevel, "silent">): boolean;
118
+ }
119
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,UAAU,GACV,UAAU,GACV,UAAU,GACV,OAAO,GACP,UAAU,GACV,SAAS,CAAC;AAEd;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,MAAM;IACrB;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,IAAI,CACF,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,IAAI,CACF,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;OAOG;IACH,KAAK,CACH,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAAC;IAER;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC;CACxD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,8 @@
1
1
  import type { ScenaristMockWithParams, ScenaristResponse, HttpRequestContext, ScenaristResult } from "../../types/index.js";
2
+ import { ScenaristError } from "../../types/errors.js";
2
3
  /**
3
4
  * Error type for response selection failures.
5
+ * @deprecated Use ScenaristError with ErrorCodes.NO_MOCK_FOUND instead
4
6
  */
5
7
  export declare class ResponseSelectionError extends Error {
6
8
  constructor(message: string);
@@ -29,6 +31,6 @@ export interface ResponseSelector {
29
31
  * @param mocks - Candidate mocks with extracted params (already filtered by URL/method)
30
32
  * @returns ScenaristResult with selected ScenaristResponse or error if no match found
31
33
  */
32
- selectResponse(testId: string, scenarioId: string, context: HttpRequestContext, mocks: ReadonlyArray<ScenaristMockWithParams>): ScenaristResult<ScenaristResponse, ResponseSelectionError>;
34
+ selectResponse(testId: string, scenarioId: string, context: HttpRequestContext, mocks: ReadonlyArray<ScenaristMockWithParams>): ScenaristResult<ScenaristResponse, ScenaristError>;
33
35
  }
34
36
  //# sourceMappingURL=response-selector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/response-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;OAQG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,aAAa,CAAC,uBAAuB,CAAC,GAC5C,eAAe,CAAC,iBAAiB,EAAE,sBAAsB,CAAC,CAAC;CAC/D"}
1
+ {"version":3,"file":"response-selector.d.ts","sourceRoot":"","sources":["../../../src/ports/driven/response-selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;OAQG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,aAAa,CAAC,uBAAuB,CAAC,GAC5C,eAAe,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;CACvD"}
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Error type for response selection failures.
3
+ * @deprecated Use ScenaristError with ErrorCodes.NO_MOCK_FOUND instead
3
4
  */
4
5
  export class ResponseSelectionError extends Error {
5
6
  constructor(message) {
@@ -5,4 +5,5 @@ export type { RequestContext } from "./driven/request-context.js";
5
5
  export type { ResponseSelector } from "./driven/response-selector.js";
6
6
  export type { SequenceTracker, SequencePosition, } from "./driven/sequence-tracker.js";
7
7
  export type { StateManager } from "./driven/state-manager.js";
8
+ export type { Logger, LogLevel, LogCategory, LogContext, LogEntry, } from "./driven/logger.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EACV,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,YAAY,EACV,MAAM,EACN,QAAQ,EACR,WAAW,EACX,UAAU,EACV,QAAQ,GACT,MAAM,oBAAoB,CAAC"}
@@ -1,4 +1,24 @@
1
1
  import type { ScenaristScenarios } from "./scenario.js";
2
+ /**
3
+ * How errors should be handled when they occur.
4
+ *
5
+ * - `throw`: Throw ScenaristError (strict - test fails with clear message)
6
+ * - `warn`: Log at warn level, return undefined (let strictMode decide next step)
7
+ * - `ignore`: Return undefined silently (let strictMode decide next step)
8
+ */
9
+ export type ErrorBehavior = "throw" | "warn" | "ignore";
10
+ /**
11
+ * Configuration for how different error types should be handled.
12
+ * Default is 'throw' for all (strict by default).
13
+ */
14
+ export type ErrorBehaviors = {
15
+ /** How to handle when no mock matches a request. Default: 'throw' */
16
+ readonly onNoMockFound: ErrorBehavior;
17
+ /** How to handle when a sequence is exhausted. Default: 'throw' */
18
+ readonly onSequenceExhausted: ErrorBehavior;
19
+ /** How to handle when x-scenarist-test-id header is missing. Default: 'throw' */
20
+ readonly onMissingTestId: ErrorBehavior;
21
+ };
2
22
  /**
3
23
  * Configuration for the scenario management system.
4
24
  * All properties are readonly for immutability.
@@ -35,6 +55,11 @@ export type ScenaristConfig = {
35
55
  * The default test ID to use when no x-scenarist-test-id header is present.
36
56
  */
37
57
  readonly defaultTestId: string;
58
+ /**
59
+ * How different error types should be handled.
60
+ * Default is 'throw' for all (strict by default).
61
+ */
62
+ readonly errorBehaviors: ErrorBehaviors;
38
63
  };
39
64
  /**
40
65
  * Partial config for user input - missing values will use defaults.
@@ -66,5 +91,9 @@ export type ScenaristConfigInput<T extends ScenaristScenarios = ScenaristScenari
66
91
  */
67
92
  readonly scenarios: T;
68
93
  readonly defaultTestId?: string;
94
+ /**
95
+ * Optional error behavior overrides. Missing values use 'throw' as default.
96
+ */
97
+ readonly errorBehaviors?: Partial<ErrorBehaviors>;
69
98
  };
70
99
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE;QAClB,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,IAC/C;IACF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,mEAAmE;IACnE,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC;IAC5C,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE;QAClB,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,kEAAkE;QAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;KAC9B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,kBAAkB,GAAG,kBAAkB,IAC/C;IACF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CACnD,CAAC"}