@criterionx/testing 0.3.1

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/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @criterionx/testing
2
+
3
+ Testing utilities for Criterion decisions. Provides property-based testing, fuzzing, and coverage analysis.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @criterionx/testing
9
+ # or
10
+ pnpm add @criterionx/testing
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **testDecision** - Test decisions with expected outcomes
16
+ - **fuzz** - Fuzz testing with random inputs
17
+ - **checkProperty** - Property-based testing with fast-check
18
+ - **coverage** - Rule coverage analysis
19
+ - **detectDeadRules** - Dead code detection
20
+
21
+ ## Usage
22
+
23
+ ### Testing Decisions
24
+
25
+ ```typescript
26
+ import { testDecision } from "@criterionx/testing";
27
+ import { riskDecision } from "./decisions";
28
+
29
+ const result = testDecision(riskDecision, {
30
+ profile: defaultProfile,
31
+ cases: [
32
+ {
33
+ name: "high risk transaction",
34
+ input: { amount: 50000, country: "US" },
35
+ expected: { status: "OK", ruleId: "high-amount" },
36
+ },
37
+ {
38
+ name: "blocked country",
39
+ input: { amount: 100, country: "NK" },
40
+ expected: { status: "OK", ruleId: "blocked-country" },
41
+ },
42
+ ],
43
+ expect: {
44
+ noUnreachableRules: true,
45
+ },
46
+ });
47
+
48
+ console.log(result.passed); // true or false
49
+ console.log(result.failures); // Array of test failures
50
+ console.log(result.rulesCovered); // Rules that were exercised
51
+ ```
52
+
53
+ ### Fuzz Testing
54
+
55
+ ```typescript
56
+ import { fuzz, fc } from "@criterionx/testing";
57
+
58
+ const result = fuzz(riskDecision, {
59
+ profile: defaultProfile,
60
+ iterations: 1000,
61
+ inputArbitrary: fc.record({
62
+ amount: fc.integer({ min: 0, max: 100000 }),
63
+ country: fc.constantFrom("US", "UK", "NK", "DE"),
64
+ }),
65
+ });
66
+
67
+ console.log(result.totalRuns); // 1000
68
+ console.log(result.failed); // Number of failures
69
+ console.log(result.ruleDistribution); // Hits per rule
70
+ ```
71
+
72
+ ### Property-Based Testing
73
+
74
+ ```typescript
75
+ import { checkProperty, fc } from "@criterionx/testing";
76
+
77
+ const result = checkProperty(riskDecision, {
78
+ profile: defaultProfile,
79
+ inputArbitrary: fc.record({
80
+ amount: fc.integer({ min: 0, max: 100000 }),
81
+ country: fc.constantFrom("US", "UK"),
82
+ }),
83
+ property: (input, result) => {
84
+ // Property: valid inputs should never return INVALID_OUTPUT
85
+ return result.status !== "INVALID_OUTPUT";
86
+ },
87
+ numRuns: 100,
88
+ });
89
+
90
+ if (!result.passed) {
91
+ console.log("Property violated with:", result.counterExample);
92
+ }
93
+ ```
94
+
95
+ ### Coverage Analysis
96
+
97
+ ```typescript
98
+ import { coverage, formatCoverageReport, meetsCoverageThreshold } from "@criterionx/testing";
99
+
100
+ const report = coverage(riskDecision, {
101
+ profile: defaultProfile,
102
+ testCases: [
103
+ { input: { amount: 100, country: "NK" } },
104
+ { input: { amount: 50000, country: "US" } },
105
+ { input: { amount: 100, country: "US" } },
106
+ ],
107
+ });
108
+
109
+ console.log(formatCoverageReport(report));
110
+ // === Rule Coverage Report ===
111
+ // Coverage: 3/3 rules (100.0%)
112
+ // Covered rules:
113
+ // ✓ blocked-country (1 hits)
114
+ // ✓ high-amount (1 hits)
115
+ // ✓ default (1 hits)
116
+
117
+ if (!meetsCoverageThreshold(report, 80)) {
118
+ throw new Error("Coverage below 80%");
119
+ }
120
+ ```
121
+
122
+ ### Dead Code Detection
123
+
124
+ ```typescript
125
+ import { detectDeadRules } from "@criterionx/testing";
126
+
127
+ const deadRules = detectDeadRules(myDecision);
128
+ if (deadRules.length > 0) {
129
+ console.warn("Dead rules detected:", deadRules);
130
+ }
131
+ ```
132
+
133
+ ## API Reference
134
+
135
+ ### `testDecision(decision, options)`
136
+
137
+ Test a decision with provided test cases.
138
+
139
+ **Options:**
140
+ - `profile` - Profile to use for evaluation
141
+ - `cases` - Array of test cases with input, profile, and expected outcomes
142
+ - `expect.noUnreachableRules` - Fail if any rules aren't covered
143
+
144
+ **Returns:** `TestDecisionResult` with `passed`, `failures`, `rulesCovered`, `rulesUncovered`
145
+
146
+ ### `fuzz(decision, options)`
147
+
148
+ Run fuzz tests on a decision.
149
+
150
+ **Options:**
151
+ - `profile` - Profile to use
152
+ - `iterations` - Number of random inputs (default: 100)
153
+ - `inputArbitrary` - fast-check arbitrary for generating inputs
154
+ - `seed` - Seed for reproducibility
155
+
156
+ **Returns:** `FuzzResult` with `totalRuns`, `passed`, `failed`, `errors`, `ruleDistribution`
157
+
158
+ ### `checkProperty(decision, options)`
159
+
160
+ Run property-based tests.
161
+
162
+ **Options:**
163
+ - `profile` - Profile to use
164
+ - `inputArbitrary` - fast-check arbitrary for inputs
165
+ - `property` - Function that returns true if property holds
166
+ - `numRuns` - Number of test runs (default: 100)
167
+ - `seed` - Seed for reproducibility
168
+
169
+ **Returns:** `{ passed, counterExample?, error? }`
170
+
171
+ ### `coverage(decision, options)`
172
+
173
+ Analyze rule coverage.
174
+
175
+ **Options:**
176
+ - `profile` - Profile to use
177
+ - `testCases` - Array of inputs to test
178
+
179
+ **Returns:** `CoverageReport` with coverage percentage and rule hit counts
180
+
181
+ ### `detectDeadRules(decision)`
182
+
183
+ Detect potentially unreachable rules.
184
+
185
+ **Returns:** Array of rule IDs that appear after catch-all rules
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,157 @@
1
+ import { Decision, Engine } from '@criterionx/core';
2
+ import * as fc from 'fast-check';
3
+ import { Arbitrary } from 'fast-check';
4
+ export { fc };
5
+
6
+ /**
7
+ * Test case for a decision
8
+ */
9
+ interface TestCase<TInput, TOutput, TProfile> {
10
+ name?: string;
11
+ input: TInput;
12
+ profile: TProfile;
13
+ expected?: {
14
+ status?: "OK" | "NO_MATCH" | "INVALID_INPUT" | "INVALID_OUTPUT";
15
+ ruleId?: string;
16
+ output?: Partial<TOutput>;
17
+ };
18
+ }
19
+ /**
20
+ * Options for testDecision
21
+ */
22
+ interface TestDecisionOptions<TInput, TOutput, TProfile> {
23
+ profile: TProfile;
24
+ cases?: TestCase<TInput, TOutput, TProfile>[];
25
+ expect?: {
26
+ /** All rules should be reachable (have at least one matching case) */
27
+ noUnreachableRules?: boolean;
28
+ /** No rules should always fail */
29
+ noDeadCode?: boolean;
30
+ };
31
+ }
32
+ /**
33
+ * Result of testDecision
34
+ */
35
+ interface TestDecisionResult {
36
+ passed: boolean;
37
+ failures: TestFailure[];
38
+ rulesCovered: string[];
39
+ rulesUncovered: string[];
40
+ }
41
+ /**
42
+ * A test failure
43
+ */
44
+ interface TestFailure {
45
+ type: "case_failed" | "unreachable_rule" | "dead_code";
46
+ message: string;
47
+ details?: {
48
+ testCase?: string;
49
+ ruleId?: string;
50
+ expected?: unknown;
51
+ actual?: unknown;
52
+ };
53
+ }
54
+ /**
55
+ * Options for fuzz testing
56
+ */
57
+ interface FuzzOptions<TInput, TProfile> {
58
+ /** Number of iterations */
59
+ iterations?: number;
60
+ /** Profile to use */
61
+ profile: TProfile;
62
+ /** Custom input arbitrary (for property-based testing) */
63
+ inputArbitrary?: Arbitrary<TInput>;
64
+ /** Seed for reproducibility */
65
+ seed?: number;
66
+ }
67
+ /**
68
+ * Result of fuzz testing
69
+ */
70
+ interface FuzzResult<TInput> {
71
+ totalRuns: number;
72
+ passed: number;
73
+ failed: number;
74
+ errors: FuzzError<TInput>[];
75
+ ruleDistribution: Record<string, number>;
76
+ }
77
+ /**
78
+ * A fuzz error
79
+ */
80
+ interface FuzzError<TInput> {
81
+ input: TInput;
82
+ error: string;
83
+ iteration: number;
84
+ }
85
+ /**
86
+ * Options for coverage analysis
87
+ */
88
+ interface CoverageOptions<TInput, TProfile> {
89
+ profile: TProfile;
90
+ testCases?: Array<{
91
+ input: TInput;
92
+ }>;
93
+ }
94
+ /**
95
+ * Coverage report
96
+ */
97
+ interface CoverageReport {
98
+ totalRules: number;
99
+ coveredRules: number;
100
+ coveragePercentage: number;
101
+ rulesCovered: string[];
102
+ rulesUncovered: string[];
103
+ ruleHits: Record<string, number>;
104
+ }
105
+
106
+ /**
107
+ * Test a decision with provided test cases and assertions
108
+ */
109
+ declare function testDecision<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>, options: TestDecisionOptions<TInput, TOutput, TProfile>): TestDecisionResult;
110
+
111
+ /**
112
+ * Fuzz test a decision with random inputs
113
+ *
114
+ * Uses fast-check for property-based testing to discover edge cases
115
+ */
116
+ declare function fuzz<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>, options: FuzzOptions<TInput, TProfile>): FuzzResult<TInput>;
117
+ /**
118
+ * Run property-based tests on a decision
119
+ *
120
+ * Allows defining custom properties that should hold for all inputs
121
+ */
122
+ declare function checkProperty<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>, options: {
123
+ profile: TProfile;
124
+ inputArbitrary: fc.Arbitrary<TInput>;
125
+ property: (input: TInput, result: ReturnType<Engine["run"]>) => boolean;
126
+ numRuns?: number;
127
+ seed?: number;
128
+ }): {
129
+ passed: boolean;
130
+ counterExample?: TInput;
131
+ error?: string;
132
+ };
133
+
134
+ /**
135
+ * Analyze rule coverage for a decision
136
+ *
137
+ * Reports which rules have been exercised by the provided test cases
138
+ */
139
+ declare function coverage<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>, options: CoverageOptions<TInput, TProfile>): CoverageReport;
140
+ /**
141
+ * Generate a coverage report as a formatted string
142
+ */
143
+ declare function formatCoverageReport(report: CoverageReport): string;
144
+ /**
145
+ * Check if coverage meets a threshold
146
+ */
147
+ declare function meetsCoverageThreshold(report: CoverageReport, threshold: number): boolean;
148
+ /**
149
+ * Analyze which rules are potentially dead code
150
+ *
151
+ * A rule is considered potentially dead if:
152
+ * 1. It was never matched in fuzzing
153
+ * 2. It comes after a rule that always matches (like a catch-all)
154
+ */
155
+ declare function detectDeadRules<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>): string[];
156
+
157
+ export { type CoverageOptions, type CoverageReport, type FuzzError, type FuzzOptions, type FuzzResult, type TestCase, type TestDecisionOptions, type TestDecisionResult, type TestFailure, checkProperty, coverage, detectDeadRules, formatCoverageReport, fuzz, meetsCoverageThreshold, testDecision };
package/dist/index.js ADDED
@@ -0,0 +1,305 @@
1
+ // src/test-decision.ts
2
+ import { Engine } from "@criterionx/core";
3
+ var engine = new Engine();
4
+ function testDecision(decision, options) {
5
+ const failures = [];
6
+ const ruleHits = /* @__PURE__ */ new Map();
7
+ for (const rule of decision.rules) {
8
+ ruleHits.set(rule.id, 0);
9
+ }
10
+ if (options.cases) {
11
+ for (const testCase of options.cases) {
12
+ const profile = testCase.profile ?? options.profile;
13
+ const result = engine.run(decision, testCase.input, { profile });
14
+ if (result.status === "OK" && result.meta.matchedRule) {
15
+ const current = ruleHits.get(result.meta.matchedRule) ?? 0;
16
+ ruleHits.set(result.meta.matchedRule, current + 1);
17
+ }
18
+ if (testCase.expected) {
19
+ const caseFailures = validateExpectations(
20
+ testCase,
21
+ result,
22
+ testCase.name
23
+ );
24
+ failures.push(...caseFailures);
25
+ }
26
+ }
27
+ }
28
+ if (options.expect?.noUnreachableRules) {
29
+ for (const [ruleId, hits] of ruleHits) {
30
+ if (hits === 0) {
31
+ failures.push({
32
+ type: "unreachable_rule",
33
+ message: `Rule "${ruleId}" was never matched by any test case`,
34
+ details: { ruleId }
35
+ });
36
+ }
37
+ }
38
+ }
39
+ const rulesCovered = [...ruleHits.entries()].filter(([, hits]) => hits > 0).map(([id]) => id);
40
+ const rulesUncovered = [...ruleHits.entries()].filter(([, hits]) => hits === 0).map(([id]) => id);
41
+ return {
42
+ passed: failures.length === 0,
43
+ failures,
44
+ rulesCovered,
45
+ rulesUncovered
46
+ };
47
+ }
48
+ function validateExpectations(testCase, result, caseName) {
49
+ const failures = [];
50
+ const { expected } = testCase;
51
+ const caseLabel = caseName ?? JSON.stringify(testCase.input);
52
+ if (!expected) return failures;
53
+ if (expected.status && result.status !== expected.status) {
54
+ failures.push({
55
+ type: "case_failed",
56
+ message: `Test case "${caseLabel}" expected status "${expected.status}" but got "${result.status}"`,
57
+ details: {
58
+ testCase: caseLabel,
59
+ expected: expected.status,
60
+ actual: result.status
61
+ }
62
+ });
63
+ }
64
+ if (expected.ruleId && result.meta.matchedRule !== expected.ruleId) {
65
+ failures.push({
66
+ type: "case_failed",
67
+ message: `Test case "${caseLabel}" expected rule "${expected.ruleId}" but got "${result.meta.matchedRule ?? "none"}"`,
68
+ details: {
69
+ testCase: caseLabel,
70
+ expected: expected.ruleId,
71
+ actual: result.meta.matchedRule
72
+ }
73
+ });
74
+ }
75
+ if (expected.output && result.data) {
76
+ for (const [key, value] of Object.entries(expected.output)) {
77
+ const actualValue = result.data[key];
78
+ if (!deepEqual(actualValue, value)) {
79
+ failures.push({
80
+ type: "case_failed",
81
+ message: `Test case "${caseLabel}" expected output.${key} to be ${JSON.stringify(value)} but got ${JSON.stringify(actualValue)}`,
82
+ details: {
83
+ testCase: caseLabel,
84
+ expected: { [key]: value },
85
+ actual: { [key]: actualValue }
86
+ }
87
+ });
88
+ }
89
+ }
90
+ }
91
+ return failures;
92
+ }
93
+ function deepEqual(a, b) {
94
+ if (a === b) return true;
95
+ if (typeof a !== typeof b) return false;
96
+ if (typeof a !== "object" || a === null || b === null) return false;
97
+ const keysA = Object.keys(a);
98
+ const keysB = Object.keys(b);
99
+ if (keysA.length !== keysB.length) return false;
100
+ for (const key of keysA) {
101
+ if (!keysB.includes(key)) return false;
102
+ if (!deepEqual(
103
+ a[key],
104
+ b[key]
105
+ ))
106
+ return false;
107
+ }
108
+ return true;
109
+ }
110
+
111
+ // src/fuzz.ts
112
+ import { Engine as Engine2 } from "@criterionx/core";
113
+ import * as fc from "fast-check";
114
+ var engine2 = new Engine2();
115
+ function fuzz(decision, options) {
116
+ const iterations = options.iterations ?? 100;
117
+ const errors = [];
118
+ const ruleDistribution = {};
119
+ let passed = 0;
120
+ let failed = 0;
121
+ for (const rule of decision.rules) {
122
+ ruleDistribution[rule.id] = 0;
123
+ }
124
+ ruleDistribution["NO_MATCH"] = 0;
125
+ ruleDistribution["INVALID_INPUT"] = 0;
126
+ const inputArbitrary = options.inputArbitrary ?? createDefaultArbitrary();
127
+ const inputs = [];
128
+ const seed = options.seed ?? Date.now();
129
+ fc.assert(
130
+ fc.property(inputArbitrary, (input) => {
131
+ inputs.push(input);
132
+ return true;
133
+ }),
134
+ {
135
+ numRuns: iterations,
136
+ seed
137
+ }
138
+ );
139
+ for (let i = 0; i < inputs.length; i++) {
140
+ const input = inputs[i];
141
+ try {
142
+ const result = engine2.run(decision, input, { profile: options.profile });
143
+ if (result.status === "OK" && result.meta.matchedRule) {
144
+ ruleDistribution[result.meta.matchedRule]++;
145
+ passed++;
146
+ } else if (result.status === "NO_MATCH") {
147
+ ruleDistribution["NO_MATCH"]++;
148
+ passed++;
149
+ } else if (result.status === "INVALID_INPUT") {
150
+ ruleDistribution["INVALID_INPUT"]++;
151
+ passed++;
152
+ } else {
153
+ failed++;
154
+ errors.push({
155
+ input,
156
+ error: result.meta.explanation,
157
+ iteration: i
158
+ });
159
+ }
160
+ } catch (error) {
161
+ failed++;
162
+ errors.push({
163
+ input,
164
+ error: String(error),
165
+ iteration: i
166
+ });
167
+ }
168
+ }
169
+ return {
170
+ totalRuns: inputs.length,
171
+ passed,
172
+ failed,
173
+ errors,
174
+ ruleDistribution
175
+ };
176
+ }
177
+ function checkProperty(decision, options) {
178
+ try {
179
+ fc.assert(
180
+ fc.property(options.inputArbitrary, (input) => {
181
+ const result = engine2.run(decision, input, {
182
+ profile: options.profile
183
+ });
184
+ return options.property(input, result);
185
+ }),
186
+ {
187
+ numRuns: options.numRuns ?? 100,
188
+ seed: options.seed
189
+ }
190
+ );
191
+ return { passed: true };
192
+ } catch (error) {
193
+ if (error instanceof Error) {
194
+ const fcError = error;
195
+ return {
196
+ passed: false,
197
+ counterExample: fcError.counterexample?.[0],
198
+ error: fcError.message
199
+ };
200
+ }
201
+ return {
202
+ passed: false,
203
+ error: String(error)
204
+ };
205
+ }
206
+ }
207
+ function createDefaultArbitrary() {
208
+ return fc.dictionary(
209
+ fc.string({ minLength: 1, maxLength: 20 }),
210
+ fc.oneof(
211
+ fc.string(),
212
+ fc.integer(),
213
+ fc.double({ noNaN: true }),
214
+ fc.boolean(),
215
+ fc.constant(null)
216
+ )
217
+ );
218
+ }
219
+
220
+ // src/coverage.ts
221
+ import { Engine as Engine3 } from "@criterionx/core";
222
+ var engine3 = new Engine3();
223
+ function coverage(decision, options) {
224
+ const ruleHits = {};
225
+ const testCases = options.testCases ?? [];
226
+ for (const rule of decision.rules) {
227
+ ruleHits[rule.id] = 0;
228
+ }
229
+ for (const testCase of testCases) {
230
+ const result = engine3.run(decision, testCase.input, {
231
+ profile: options.profile
232
+ });
233
+ if (result.status === "OK" && result.meta.matchedRule) {
234
+ ruleHits[result.meta.matchedRule]++;
235
+ }
236
+ }
237
+ const totalRules = decision.rules.length;
238
+ const rulesCovered = Object.entries(ruleHits).filter(([, hits]) => hits > 0).map(([id]) => id);
239
+ const rulesUncovered = Object.entries(ruleHits).filter(([, hits]) => hits === 0).map(([id]) => id);
240
+ const coveredRules = rulesCovered.length;
241
+ const coveragePercentage = totalRules > 0 ? coveredRules / totalRules * 100 : 100;
242
+ return {
243
+ totalRules,
244
+ coveredRules,
245
+ coveragePercentage,
246
+ rulesCovered,
247
+ rulesUncovered,
248
+ ruleHits
249
+ };
250
+ }
251
+ function formatCoverageReport(report) {
252
+ const lines = [];
253
+ lines.push("=== Rule Coverage Report ===");
254
+ lines.push("");
255
+ lines.push(
256
+ `Coverage: ${report.coveredRules}/${report.totalRules} rules (${report.coveragePercentage.toFixed(1)}%)`
257
+ );
258
+ lines.push("");
259
+ if (report.rulesCovered.length > 0) {
260
+ lines.push("Covered rules:");
261
+ for (const ruleId of report.rulesCovered) {
262
+ const hits = report.ruleHits[ruleId];
263
+ lines.push(` \u2713 ${ruleId} (${hits} hits)`);
264
+ }
265
+ }
266
+ if (report.rulesUncovered.length > 0) {
267
+ lines.push("");
268
+ lines.push("Uncovered rules:");
269
+ for (const ruleId of report.rulesUncovered) {
270
+ lines.push(` \u2717 ${ruleId}`);
271
+ }
272
+ }
273
+ return lines.join("\n");
274
+ }
275
+ function meetsCoverageThreshold(report, threshold) {
276
+ return report.coveragePercentage >= threshold;
277
+ }
278
+ function detectDeadRules(decision) {
279
+ const deadRules = [];
280
+ let foundCatchAll = false;
281
+ for (const rule of decision.rules) {
282
+ if (foundCatchAll) {
283
+ deadRules.push(rule.id);
284
+ continue;
285
+ }
286
+ const whenStr = rule.when.toString();
287
+ if (whenStr.includes("=> true") || whenStr.includes("return true") || whenStr.includes("()=>true")) {
288
+ foundCatchAll = true;
289
+ }
290
+ }
291
+ return deadRules;
292
+ }
293
+
294
+ // src/index.ts
295
+ import * as fc2 from "fast-check";
296
+ export {
297
+ checkProperty,
298
+ coverage,
299
+ detectDeadRules,
300
+ fc2 as fc,
301
+ formatCoverageReport,
302
+ fuzz,
303
+ meetsCoverageThreshold,
304
+ testDecision
305
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@criterionx/testing",
3
+ "version": "0.3.1",
4
+ "description": "Testing utilities for Criterion decisions",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --dts --clean",
15
+ "dev": "tsup src/index.ts --format esm --dts --watch",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "criterion",
23
+ "testing",
24
+ "decision-engine",
25
+ "fuzzing",
26
+ "property-based-testing"
27
+ ],
28
+ "author": {
29
+ "name": "Tomas Maritano",
30
+ "url": "https://github.com/tomymaritano"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/tomymaritano/criterionx.git",
36
+ "directory": "packages/testing"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/tomymaritano/criterionx/issues"
40
+ },
41
+ "homepage": "https://github.com/tomymaritano/criterionx#readme",
42
+ "dependencies": {
43
+ "@criterionx/core": "workspace:*",
44
+ "fast-check": "^3.23.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.3.0",
50
+ "vitest": "^4.0.0",
51
+ "zod": "^3.23.0"
52
+ },
53
+ "peerDependencies": {
54
+ "zod": "^3.0.0"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ }
59
+ }