@infitx/decision 0.0.1 → 1.3.2

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 (4) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +432 -0
  3. package/index.js +35 -0
  4. package/package.json +21 -8
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## [1.3.2](https://github.com/infitx-org/release-cd/compare/decision-v1.3.1...decision-v1.3.2) (2026-02-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * update package versions and permissions in workflows ([c582d9f](https://github.com/infitx-org/release-cd/commit/c582d9faff24e6a0549e529608f0ef6a8b362bcf))
9
+
10
+ ## [1.3.1](https://github.com/infitx-org/release-cd/compare/decision-v1.3.0...decision-v1.3.1) (2026-02-09)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * deploy ([9aa2bd6](https://github.com/infitx-org/release-cd/commit/9aa2bd69093d4374f86dd8554a527b627032c326))
16
+
17
+ ## [1.3.0](https://github.com/infitx-org/release-cd/compare/decision-v1.2.0...decision-v1.3.0) (2026-02-09)
18
+
19
+
20
+ ### Features
21
+
22
+ * add .npmignore files and ci-publish script for decision and match libraries ([c518708](https://github.com/infitx-org/release-cd/commit/c5187080c63a215a670fcbe37a11989d6ec4c37f))
23
+
24
+ ## [1.2.0](https://github.com/infitx-org/release-cd/compare/decision-v1.1.0...decision-v1.2.0) (2026-01-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * implement multiple decisions ([74cb3b6](https://github.com/infitx-org/release-cd/commit/74cb3b6bcc5cc15fb59fd28843cf920897b27c9c))
30
+
31
+ ## [1.1.0](https://github.com/infitx-org/release-cd/compare/decision-v1.0.0...decision-v1.1.0) (2025-12-23)
32
+
33
+
34
+ ### Features
35
+
36
+ * add build script to package.json for decision and match libraries ([f4da999](https://github.com/infitx-org/release-cd/commit/f4da9993feec346cc94f573fa103dffa8c4a9eed))
37
+ * match and decision libraries ([db62341](https://github.com/infitx-org/release-cd/commit/db623419a179b3e0ec0cbda05b2f135e01375552))
38
+ * update decision module to use YAML for configuration and improve rule handling ([92fa802](https://github.com/infitx-org/release-cd/commit/92fa80243508ff486c3e39f1b32d90701c2eb5d6))
package/README.md ADDED
@@ -0,0 +1,432 @@
1
+ # Decision Library
2
+
3
+ A lightweight rule engine for evaluating facts against configurable rules and
4
+ generating decisions. The library uses pattern matching to determine which rules
5
+ apply to a given fact and returns corresponding decisions.
6
+
7
+ ## Features
8
+
9
+ - **Pattern Matching**: Uses `@infitx/match` for flexible fact matching
10
+ - **Priority-based Evaluation**: Rules can have explicit or implicit priorities
11
+ - **Multiple Decision Support**: Can return first match or all matching rules
12
+ - **YAML Configuration**: Define rules in human-readable YAML format
13
+ - **Type Safety**: Built-in support for various data types including dates and timestamps
14
+
15
+ ## Installation
16
+
17
+ ```javascript
18
+ const decision = require('./');
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ```javascript
24
+ const decision = require('./');
25
+ const { decide, rules } = decision('./config.yaml');
26
+
27
+ const fact = { type: 'transfer', amount: 500 };
28
+ const result = decide(fact);
29
+ console.log(result);
30
+ ```
31
+
32
+ ## Examples
33
+
34
+ ### Example 1: Single Decision with Implicit Priority
35
+
36
+ Rules are evaluated in the order they appear in the configuration. When rules
37
+ are defined as an object, the key becomes the rule name and serves as the
38
+ implicit priority (alphabetically sorted).
39
+
40
+ - - YAML Configuration
41
+
42
+ ```yaml
43
+ rules:
44
+ transfer-approval:
45
+ when: { type: transfer, amount: { max: 1000 } }
46
+ then: { expect: { approved: true, reason: approved } }
47
+
48
+ transfer-rejection:
49
+ when: { type: transfer, amount: { min: 1001 } }
50
+ then: { expect: { approved: false, reason: limit } }
51
+ ```
52
+
53
+ - Decision Table
54
+
55
+ | Rule | Condition | Decision |
56
+ |--------------------- |------------------------------------- |----------------------------------------------- |
57
+ |transfer-approval | type=transfer AND amount ≤ 1000 | expect: approved=true, reason=approved |
58
+ |transfer-rejection | type=transfer AND amount ≥ 1001 | expect: approved=false, reason=limit |
59
+
60
+ - Code Example
61
+
62
+ ```javascript
63
+ const decision = require('./');
64
+ const { decide } = decision('./transfer-config.yaml');
65
+
66
+ // Test case 1: Approved transfer
67
+ const fact1 = { type: 'transfer', amount: 500 };
68
+ console.log(decide(fact1));
69
+ // Output: [{
70
+ // rule: 'transfer-approval',
71
+ // decision: 'expect',
72
+ // approved: true,
73
+ // reason: 'approved'
74
+ // }]
75
+
76
+ // Test case 2: Rejected transfer
77
+ const fact2 = { type: 'transfer', amount: 1500 };
78
+ console.log(decide(fact2));
79
+ // Output: [{
80
+ // rule: 'transfer-rejection',
81
+ // decision: 'expect',
82
+ // approved: false,
83
+ // reason: 'limit'
84
+ // }]
85
+ ```
86
+
87
+ ### Example 2: Single Decision with Explicit Priority
88
+
89
+ When you need specific evaluation order, pass an array of rules with
90
+ the required order.
91
+
92
+ - YAML Configuration
93
+
94
+ ```yaml
95
+ rules:
96
+ - rule: high-value-check
97
+ when: { type: transaction, amount: { min: 10000 } }
98
+ then: { require: { manualReview: true, approvers: 2 } }
99
+
100
+ - rule: standard-check
101
+ when: { type: transaction, amount: { max: 9999 } }
102
+ then: { allow: { automated: true, approvers: 0 } }
103
+ ```
104
+
105
+ - Decision Table
106
+
107
+ | Rule | Condition | Decision |
108
+ |-------------------|--------------------------------------|-----------------------------------------------|
109
+ | high-value-check | type=transaction AND amount ≥ 10000 | require: manualReview=true, approvers=2 |
110
+ | standard-check | type=transaction AND amount ≤ 9999 | allow: automated=true, approvers=0 |
111
+
112
+ - Code Example
113
+
114
+ ```javascript
115
+ const decision = require('./');
116
+ const { decide, rules } = decision('./priority-config.yaml');
117
+
118
+ console.log('Rules in evaluation order:', rules.map(r => ({ rule: r.rule })));
119
+ // Output: [
120
+ // { rule: 'high-value-check' },
121
+ // { rule: 'standard-check }
122
+ // ]
123
+
124
+ const fact1 = { type: 'transaction', amount: 15000 };
125
+ console.log(decide(fact1));
126
+ // Output: [{
127
+ // rule: 'high-value-check',
128
+ // decision: 'require',
129
+ // manualReview: true,
130
+ // approvers: 2
131
+ // }]
132
+
133
+ const fact2 = { type: 'transaction', amount: 500 };
134
+ console.log(decide(fact2));
135
+ // Output: [{
136
+ // rule: 'standard-check',
137
+ // decision: 'allow',
138
+ // automated: true,
139
+ // approvers: 0
140
+ // }]
141
+ ```
142
+
143
+ ### Example 3: Multiple Decisions
144
+
145
+ A single fact can trigger multiple decisions from one rule. This is useful for
146
+ scenarios where you need to take multiple actions based on a single condition.
147
+
148
+ - YAML Configuration
149
+
150
+ ```yaml
151
+ rules:
152
+ withdrawal-high-amount:
153
+ when: { type: withdrawal, amount: { min: 501 } }
154
+ then:
155
+ expect: { approved: false, reason: large }
156
+ notification: { type: alert, recipient: manager }
157
+ audit: { level: warning, message: "Large withdrawal attempted" }
158
+
159
+ withdrawal-normal:
160
+ when: { type: withdrawal, amount: { max: 500 } }
161
+ then:
162
+ expect: { approved: true, reason: small }
163
+ audit: { level: info, message: "Withdrawal approved" }
164
+ ```
165
+
166
+ - Decision Table
167
+
168
+ | Rule | Condition | Decisions |
169
+ |------------------------|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
170
+ | withdrawal-high-amount | type=withdrawal AND amount ≥ 501 | 1. expect: approved=false, reason=large<br>2. notification: type=alert, recipient=manager<br>3. audit: level=warning, message=... |
171
+ | withdrawal-normal | type=withdrawal AND amount ≤ 500 | 1. expect: approved=true, reason=small<br>2. audit: level=info, message=... |
172
+
173
+ - Code Example
174
+
175
+ ```javascript
176
+ const decision = require('./');
177
+ const { decide } = decision('./withdrawal-config.yaml');
178
+
179
+ // Large withdrawal - triggers multiple decisions
180
+ const fact1 = { type: 'withdrawal', amount: 700 };
181
+ console.log(decide(fact1));
182
+ // Output: [
183
+ // {
184
+ // rule: 'withdrawal-high-amount',
185
+ // decision: 'expect',
186
+ // approved: false,
187
+ // reason: 'large'
188
+ // }, {
189
+ // rule: 'withdrawal-high-amount',
190
+ // decision: 'notification',
191
+ // type: 'alert',
192
+ // recipient: 'manager'
193
+ // }, {
194
+ // rule: 'withdrawal-high-amount',
195
+ // decision: 'audit',
196
+ // level: 'warning',
197
+ // message: 'Large withdrawal attempted'
198
+ // }
199
+ // ]
200
+
201
+ // Normal withdrawal
202
+ const fact2 = { type: 'withdrawal', amount: 300 };
203
+ console.log(decide(fact2));
204
+ // Output: [
205
+ // {
206
+ // rule: 'withdrawal-normal',
207
+ // decision: 'expect',
208
+ // approved: true,
209
+ // reason: 'small'
210
+ // }, {
211
+ // rule: 'withdrawal-normal',
212
+ // decision: 'audit',
213
+ // level: 'info',
214
+ // message: 'Withdrawal approved'
215
+ // }
216
+ // ]
217
+ ```
218
+
219
+ ### Example 4: All Matching Rules
220
+
221
+ By default, `decide()` returns only the first matching rule. Use the `all`
222
+ parameter to get all matching rules.
223
+
224
+ - YAML Configuration
225
+
226
+ ```yaml
227
+ rules:
228
+ fraud-check:
229
+ when: { type: transaction, amount: { min: 5000 } }
230
+ then: { verify: { fraudCheck: true } }
231
+
232
+ compliance-check:
233
+ when: { type: transaction, amount: { min: 3000 } }
234
+ then: { verify: { amlCheck: true } }
235
+
236
+ standard-process:
237
+ when: { type: transaction }
238
+ then: { process: { standard: true } }
239
+ ```
240
+
241
+ - Decision Table
242
+
243
+ | Rule | Condition | Decision |
244
+ |-------------------|--------------------------------------|-------------------------------------|
245
+ | fraud-check | type=transaction AND amount ≥ 5000 | verify: fraudCheck=true |
246
+ | compliance-check | type=transaction AND amount ≥ 3000 | verify: amlCheck=true |
247
+ | standard-process | type=transaction | process: standard=true |
248
+
249
+ - Code Example
250
+
251
+ ```javascript
252
+ const decision = require('./');
253
+ const { decide } = decision('./all-rules-config.yaml');
254
+
255
+ const fact = { type: 'transaction', amount: 6000 };
256
+
257
+ // Get only first matching rule (default)
258
+ console.log(decide(fact));
259
+ // Output: [{ rule: 'fraud-check', decision: 'verify', fraudCheck: true }]
260
+
261
+ // Get all matching rules
262
+ console.log(decide(fact, true));
263
+ // Output: [
264
+ // { rule: 'fraud-check', decision: 'verify', fraudCheck: true },
265
+ // { rule: 'compliance-check', decision: 'verify', amlCheck: true },
266
+ // { rule: 'standard-process', decision: 'process', standard: true }
267
+ // ]
268
+ ```
269
+
270
+ ## YAML Configuration Format
271
+
272
+ ### Basic Structure
273
+
274
+ ```yaml
275
+ rules:
276
+ rule-name:
277
+ when: <condition>
278
+ then: <decision>
279
+ priority: <number or string> # Optional, defaults to rule name if omitted
280
+ ```
281
+
282
+ Or as an array:
283
+
284
+ ```yaml
285
+ rules:
286
+ - rule: rule-name
287
+ when: <condition>
288
+ then: <decision>
289
+ ```
290
+
291
+ ### Components
292
+
293
+ - `rules`
294
+
295
+ Can be either:
296
+
297
+ - **Object**: Keys are rule names, values are rule definitions. Rules are sorted
298
+ alphabetically by name (implicit priority) or by `priority` field if present.
299
+ - **Array**: Rules definitions with explicit order.
300
+ When an array is used, the processing order is the order of items in the array.
301
+
302
+ - `when` (Condition)
303
+
304
+ A pattern matching object that defines when a rule applies. Supports:
305
+
306
+ - **Exact match**: `{ type: transfer }`
307
+ - **Range match**: `{ amount: { min: 100, max: 1000 } }`
308
+ - **Minimum**: `{ amount: { min: 1000 } }`
309
+ - **Maximum**: `{ amount: { max: 1000 } }`
310
+ - **Complex patterns**: Nested objects, arrays, and any pattern supported by `@infitx/match`
311
+
312
+ - `then` (Decision)
313
+
314
+ Defines the outcomes when a rule matches. Structure:
315
+
316
+ ```yaml
317
+ then:
318
+ <decision-type>: <decision-value>
319
+ <another-decision>: <another-value>
320
+ ```
321
+
322
+ Each decision becomes an object in the result array:
323
+
324
+ ```javascript
325
+ { rule: 'rule-name', decision: 'decision-type', ...decision-value }
326
+ ```
327
+
328
+ - `priority` (Optional)
329
+
330
+ - **Numeric**: Lower numbers evaluated first
331
+ - **String**: Lexicographic ordering
332
+ - **Implicit**: If omitted, rule name is used as priority
333
+
334
+ ### Complete Example
335
+
336
+ ```yaml
337
+ # Object format with implicit priority
338
+ rules:
339
+ high-priority-rule:
340
+ when: { urgent: true }
341
+ then: { action: { escalate: true } }
342
+
343
+ low-priority-rule:
344
+ when: { urgent: false }
345
+ then: { action: { queue: true } }
346
+
347
+ ---
348
+
349
+ # Array format with explicit priority
350
+ rules:
351
+ - rule: critical
352
+ when: { severity: critical }
353
+ then:
354
+ alert: { immediate: true }
355
+ notify: { team: on call }
356
+ - rule: warning
357
+ when: { severity: warning }
358
+ then:
359
+ alert: { delayed: true }
360
+ - rule: info
361
+ when: { severity: info }
362
+ then:
363
+ log: { level: info }
364
+ ```
365
+
366
+ ## API Reference
367
+
368
+ ### `decision(config)`
369
+
370
+ Creates a decision engine instance.
371
+
372
+ **Parameters:**
373
+
374
+ - `config` (String | Object): Path to YAML file or configuration object
375
+
376
+ **Returns:**
377
+ Object with:
378
+
379
+ - `rules`: Array of rules in evaluation order
380
+ - `decide(fact, all)`: Function to evaluate facts
381
+ - `tests`: Test cases from configuration (if present)
382
+
383
+ ### `decide(fact, all = false)`
384
+
385
+ Evaluates a fact against configured rules.
386
+
387
+ **Parameters:**
388
+
389
+ - `fact` (Object): The fact to evaluate
390
+ - `all` (Boolean): If true, returns all matching rules; if false, returns
391
+ only first match
392
+
393
+ **Returns:**
394
+ Array of decision objects:
395
+
396
+ ```javascript
397
+ [
398
+ { rule: 'rule-name', decision: 'decision-type', ...decision-value }
399
+ ]
400
+ ```
401
+
402
+ Returns `null` (or empty array if `all=true`) if no rules match.
403
+
404
+ ## Testing
405
+
406
+ The library supports inline test definitions in YAML:
407
+
408
+ ```yaml
409
+ rules:
410
+ # ... rule definitions
411
+
412
+ tests:
413
+ - fact: { type: transfer, amount: 500 }
414
+ expected:
415
+ - approved: true
416
+ reason: approved
417
+ rule: transfer-approval
418
+ decision: expect
419
+ ```
420
+
421
+ Run tests with Jest:
422
+
423
+ ```javascript
424
+ const decision = require('./');
425
+ const { decide, tests } = decision('./config.yaml');
426
+
427
+ tests.forEach(({ fact, expected }) => {
428
+ test(`fact: ${JSON.stringify(fact)}`, () => {
429
+ expect(decide(fact)).toEqual(expected);
430
+ });
431
+ });
432
+ ```
package/index.js ADDED
@@ -0,0 +1,35 @@
1
+ const match = require('@infitx/match');
2
+ const yaml = require('yaml');
3
+ const fs = require('fs');
4
+
5
+ module.exports = function decision(config) {
6
+ if (typeof config === 'string') {
7
+ config = yaml.parse(fs.readFileSync(config, 'utf8'), { customTags: ['timestamp'] });
8
+ }
9
+ let rules = config.rules ?? config;
10
+ if (!Array.isArray(rules)) rules = Object.entries(rules).map(([rule, value]) => ({ rule, ...value })).sort((a, b) => {
11
+ const aPriority = a.priority ?? a.rule;
12
+ const bPriority = b.priority ?? b.rule;
13
+ if (aPriority < bPriority) return -1;
14
+ if (aPriority > bPriority) return 1;
15
+ return 0;
16
+ });
17
+ return {
18
+ ...config,
19
+ rules,
20
+ decide: (fact, all) => {
21
+ const decisions = all ? [] : null;
22
+ for (const index in rules) {
23
+ const { rule = index, when, then } = rules[index];
24
+ if (match(fact, when)) {
25
+ if (all) {
26
+ Object.entries(typeof then === 'function' ? then(fact) : then).forEach(([decision, value]) => decisions.push({ rule, decision, ...value }));
27
+ } else {
28
+ return Object.entries(typeof then === 'function' ? then(fact) : then).map(([decision, value]) => ({ rule, decision, ...value }));
29
+ }
30
+ }
31
+ }
32
+ return decisions;
33
+ }
34
+ }
35
+ };
package/package.json CHANGED
@@ -1,12 +1,25 @@
1
1
  {
2
2
  "name": "@infitx/decision",
3
- "version": "0.0.1",
4
- "description": "",
5
- "license": "ISC",
6
- "author": "",
7
- "type": "commonjs",
3
+ "version": "1.3.2",
4
+ "description": "Decision tables engine",
8
5
  "main": "index.js",
9
6
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- }
12
- }
7
+ "build": "true",
8
+ "ci-unit": "JEST_JUNIT_OUTPUT_DIR=coverage jest --ci --reporters=default --reporters=jest-junit --outputFile=./coverage/junit.xml",
9
+ "ci-publish": "npm publish --access public --provenance",
10
+ "watch": "jest --watchAll"
11
+ },
12
+ "dependencies": {
13
+ "@infitx/match": "workspace:*",
14
+ "yaml": "^2.8.1"
15
+ },
16
+ "devDependencies": {
17
+ "jest": "^30.2.0",
18
+ "jest-junit": "^16.0.0"
19
+ },
20
+ "repository": {
21
+ "url": "git+https://github.com/infitx-org/release-cd.git"
22
+ },
23
+ "author": "Kalin Krustev",
24
+ "license": "Apache-2.0"
25
+ }