@sebasoft/neuron-js 0.3.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.
- package/README.md +84 -45
- package/dist/commonjs/Synapse.d.ts +18 -0
- package/dist/commonjs/Synapse.d.ts.map +1 -1
- package/dist/commonjs/Synapse.js +18 -0
- package/dist/commonjs/Synapse.js.map +1 -1
- package/dist/commonjs/abstracts/AbstractAction.d.ts +15 -0
- package/dist/commonjs/abstracts/AbstractAction.d.ts.map +1 -0
- package/dist/commonjs/abstracts/AbstractAction.js +25 -0
- package/dist/commonjs/abstracts/AbstractAction.js.map +1 -0
- package/dist/commonjs/abstracts/AbstractCondition.d.ts +15 -0
- package/dist/commonjs/abstracts/AbstractCondition.d.ts.map +1 -0
- package/dist/commonjs/abstracts/AbstractCondition.js +25 -0
- package/dist/commonjs/abstracts/AbstractCondition.js.map +1 -0
- package/dist/commonjs/contracts/explain.d.ts +9 -0
- package/dist/commonjs/contracts/explain.d.ts.map +1 -0
- package/dist/commonjs/contracts/explain.js +51 -0
- package/dist/commonjs/contracts/explain.js.map +1 -0
- package/dist/commonjs/contracts/validation.d.ts +40 -0
- package/dist/commonjs/contracts/validation.d.ts.map +1 -0
- package/dist/commonjs/contracts/validation.js +263 -0
- package/dist/commonjs/contracts/validation.js.map +1 -0
- package/dist/commonjs/index.d.ts +64 -0
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +74 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/interfaces/Action.d.ts +14 -0
- package/dist/commonjs/interfaces/Action.d.ts.map +1 -1
- package/dist/commonjs/interfaces/Condition.d.ts +21 -0
- package/dist/commonjs/interfaces/Condition.d.ts.map +1 -1
- package/dist/commonjs/interfaces/Element.d.ts +16 -0
- package/dist/commonjs/interfaces/Element.d.ts.map +1 -1
- package/dist/commonjs/interfaces/HookEvents.d.ts +16 -0
- package/dist/commonjs/interfaces/HookEvents.d.ts.map +1 -1
- package/dist/commonjs/interfaces/HookEvents.js +16 -0
- package/dist/commonjs/interfaces/HookEvents.js.map +1 -1
- package/dist/commonjs/interfaces/Parameter.d.ts +16 -0
- package/dist/commonjs/interfaces/Parameter.d.ts.map +1 -1
- package/dist/commonjs/interfaces/Rule.d.ts +23 -0
- package/dist/commonjs/interfaces/Rule.d.ts.map +1 -1
- package/dist/commonjs/interfaces/Script.d.ts +10 -0
- package/dist/commonjs/interfaces/Script.d.ts.map +1 -1
- package/dist/commonjs/runtime/ActionRuntime.js +1 -1
- package/dist/commonjs/runtime/ActionRuntime.js.map +1 -1
- package/dist/commonjs/runtime/ConditionRuntime.js +3 -3
- package/dist/commonjs/runtime/ConditionRuntime.js.map +1 -1
- package/dist/commonjs/runtime/RuleRuntime.js +1 -1
- package/dist/commonjs/runtime/RuleRuntime.js.map +1 -1
- package/dist/commonjs/types/ExecutionContext.d.ts +13 -0
- package/dist/commonjs/types/ExecutionContext.d.ts.map +1 -1
- package/dist/commonjs/types/ExecutionContext.js +3 -0
- package/dist/commonjs/types/ExecutionContext.js.map +1 -1
- package/dist/commonjs/types/ExecutionResult.d.ts +14 -0
- package/dist/commonjs/types/ExecutionResult.d.ts.map +1 -1
- package/dist/commonjs/types/ExecutionResult.js +14 -0
- package/dist/commonjs/types/ExecutionResult.js.map +1 -1
- package/dist/commonjs/types/HookEmitter.d.ts +8 -1
- package/dist/commonjs/types/HookEmitter.d.ts.map +1 -1
- package/dist/esm/Synapse.d.ts +18 -0
- package/dist/esm/Synapse.d.ts.map +1 -1
- package/dist/esm/Synapse.js +18 -0
- package/dist/esm/Synapse.js.map +1 -1
- package/dist/esm/abstracts/AbstractAction.d.ts +15 -0
- package/dist/esm/abstracts/AbstractAction.d.ts.map +1 -0
- package/dist/esm/abstracts/AbstractAction.js +21 -0
- package/dist/esm/abstracts/AbstractAction.js.map +1 -0
- package/dist/esm/abstracts/AbstractCondition.d.ts +15 -0
- package/dist/esm/abstracts/AbstractCondition.d.ts.map +1 -0
- package/dist/esm/abstracts/AbstractCondition.js +21 -0
- package/dist/esm/abstracts/AbstractCondition.js.map +1 -0
- package/dist/esm/contracts/explain.d.ts +9 -0
- package/dist/esm/contracts/explain.d.ts.map +1 -0
- package/dist/esm/contracts/explain.js +48 -0
- package/dist/esm/contracts/explain.js.map +1 -0
- package/dist/esm/contracts/validation.d.ts +40 -0
- package/dist/esm/contracts/validation.d.ts.map +1 -0
- package/dist/esm/contracts/validation.js +255 -0
- package/dist/esm/contracts/validation.js.map +1 -0
- package/dist/esm/index.d.ts +64 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interfaces/Action.d.ts +14 -0
- package/dist/esm/interfaces/Action.d.ts.map +1 -1
- package/dist/esm/interfaces/Condition.d.ts +21 -0
- package/dist/esm/interfaces/Condition.d.ts.map +1 -1
- package/dist/esm/interfaces/Element.d.ts +16 -0
- package/dist/esm/interfaces/Element.d.ts.map +1 -1
- package/dist/esm/interfaces/HookEvents.d.ts +16 -0
- package/dist/esm/interfaces/HookEvents.d.ts.map +1 -1
- package/dist/esm/interfaces/HookEvents.js +16 -0
- package/dist/esm/interfaces/HookEvents.js.map +1 -1
- package/dist/esm/interfaces/Parameter.d.ts +16 -0
- package/dist/esm/interfaces/Parameter.d.ts.map +1 -1
- package/dist/esm/interfaces/Rule.d.ts +23 -0
- package/dist/esm/interfaces/Rule.d.ts.map +1 -1
- package/dist/esm/interfaces/Script.d.ts +10 -0
- package/dist/esm/interfaces/Script.d.ts.map +1 -1
- package/dist/esm/runtime/ActionRuntime.js +1 -1
- package/dist/esm/runtime/ActionRuntime.js.map +1 -1
- package/dist/esm/runtime/ConditionRuntime.js +3 -3
- package/dist/esm/runtime/ConditionRuntime.js.map +1 -1
- package/dist/esm/runtime/RuleRuntime.js +1 -1
- package/dist/esm/runtime/RuleRuntime.js.map +1 -1
- package/dist/esm/types/ExecutionContext.d.ts +13 -0
- package/dist/esm/types/ExecutionContext.d.ts.map +1 -1
- package/dist/esm/types/ExecutionContext.js +3 -0
- package/dist/esm/types/ExecutionContext.js.map +1 -1
- package/dist/esm/types/ExecutionResult.d.ts +14 -0
- package/dist/esm/types/ExecutionResult.d.ts.map +1 -1
- package/dist/esm/types/ExecutionResult.js +14 -0
- package/dist/esm/types/ExecutionResult.js.map +1 -1
- package/dist/esm/types/HookEmitter.d.ts +8 -1
- package/dist/esm/types/HookEmitter.d.ts.map +1 -1
- package/examples/README.md +24 -0
- package/examples/eligibility-check/README.md +31 -0
- package/examples/eligibility-check/expected-output.json +7 -0
- package/examples/eligibility-check/input.json +6 -0
- package/examples/eligibility-check/rules.json +32 -0
- package/examples/eligibility-check/run.ts +128 -0
- package/examples/pricing-rules/README.md +31 -0
- package/examples/pricing-rules/expected-output.json +7 -0
- package/examples/pricing-rules/input.json +7 -0
- package/examples/pricing-rules/rules.json +32 -0
- package/examples/pricing-rules/run.ts +136 -0
- package/examples/workflow-routing/README.md +31 -0
- package/examples/workflow-routing/expected-output.json +7 -0
- package/examples/workflow-routing/input.json +6 -0
- package/examples/workflow-routing/rules.json +33 -0
- package/examples/workflow-routing/run.ts +130 -0
- package/package.json +31 -4
- package/schemas/execution-context.schema.json +23 -0
- package/schemas/execution-output.schema.json +16 -0
- package/schemas/explanation-trace.schema.json +32 -0
- package/schemas/script.schema.json +90 -0
- package/schemas/validation-error.schema.json +13 -0
- package/src/Synapse.ts +18 -0
- package/src/abstracts/AbstractAction.ts +34 -0
- package/src/abstracts/AbstractCondition.ts +34 -0
- package/src/contracts/explain.ts +66 -0
- package/src/contracts/validation.ts +348 -0
- package/src/index.ts +116 -0
- package/src/interfaces/Action.ts +14 -0
- package/src/interfaces/Condition.ts +23 -0
- package/src/interfaces/Element.ts +18 -0
- package/src/interfaces/HookEvents.ts +16 -0
- package/src/interfaces/Parameter.ts +18 -0
- package/src/interfaces/Rule.ts +24 -0
- package/src/interfaces/Script.ts +11 -0
- package/src/runtime/ActionRuntime.ts +1 -1
- package/src/runtime/ConditionRuntime.ts +3 -3
- package/src/runtime/RuleRuntime.ts +1 -1
- package/src/types/ExecutionContext.ts +13 -0
- package/src/types/ExecutionResult.ts +14 -0
- package/src/types/HookEmitter.ts +5 -0
- package/dist/commonjs/Synapse.test.d.ts +0 -2
- package/dist/commonjs/Synapse.test.d.ts.map +0 -1
- package/dist/commonjs/Synapse.test.js +0 -15
- package/dist/commonjs/Synapse.test.js.map +0 -1
- package/dist/commonjs/index.test.d.ts +0 -2
- package/dist/commonjs/index.test.d.ts.map +0 -1
- package/dist/commonjs/index.test.js +0 -132
- package/dist/commonjs/index.test.js.map +0 -1
- package/dist/commonjs/runtime/ConditionRuntime.test.d.ts +0 -2
- package/dist/commonjs/runtime/ConditionRuntime.test.d.ts.map +0 -1
- package/dist/commonjs/runtime/ConditionRuntime.test.js +0 -70
- package/dist/commonjs/runtime/ConditionRuntime.test.js.map +0 -1
- package/dist/esm/Synapse.test.d.ts +0 -2
- package/dist/esm/Synapse.test.d.ts.map +0 -1
- package/dist/esm/Synapse.test.js +0 -13
- package/dist/esm/Synapse.test.js.map +0 -1
- package/dist/esm/index.test.d.ts +0 -2
- package/dist/esm/index.test.d.ts.map +0 -1
- package/dist/esm/index.test.js +0 -130
- package/dist/esm/index.test.js.map +0 -1
- package/dist/esm/runtime/ConditionRuntime.test.d.ts +0 -2
- package/dist/esm/runtime/ConditionRuntime.test.d.ts.map +0 -1
- package/dist/esm/runtime/ConditionRuntime.test.js +0 -68
- package/dist/esm/runtime/ConditionRuntime.test.js.map +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionResult,
|
|
3
|
+
MessageType,
|
|
4
|
+
Neuron,
|
|
5
|
+
Synapse,
|
|
6
|
+
type ActionOptions,
|
|
7
|
+
type ExecutionContext,
|
|
8
|
+
type ParameterInterface,
|
|
9
|
+
} from "../../dist/esm/index.js";
|
|
10
|
+
import expectedOutput from "./expected-output.json" with { type: "json" };
|
|
11
|
+
import input from "./input.json" with { type: "json" };
|
|
12
|
+
import script from "./rules.json" with { type: "json" };
|
|
13
|
+
|
|
14
|
+
function readStatePath(context: ExecutionContext, path: string): unknown {
|
|
15
|
+
return path.split(".").reduce<unknown>((current, segment) => {
|
|
16
|
+
if (current && typeof current === "object" && segment in current) {
|
|
17
|
+
return (current as Record<string, unknown>)[segment];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}, context.state);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class StateNumberParameter {
|
|
24
|
+
static readonly TYPE = "state_number";
|
|
25
|
+
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly value: string;
|
|
30
|
+
readonly options: Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
id: string,
|
|
34
|
+
type: string,
|
|
35
|
+
name: string,
|
|
36
|
+
value: string,
|
|
37
|
+
options: Record<string, unknown>,
|
|
38
|
+
) {
|
|
39
|
+
this.id = id;
|
|
40
|
+
this.type = type;
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.value = value;
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getValue(context: ExecutionContext): number | null {
|
|
47
|
+
const value = readStatePath(context, this.value);
|
|
48
|
+
return typeof value === "number" ? value : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class SetDecisionAction {
|
|
53
|
+
static readonly TYPE = "set_decision";
|
|
54
|
+
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly type: string;
|
|
57
|
+
private readonly params: ParameterInterface[];
|
|
58
|
+
readonly options: ActionOptions;
|
|
59
|
+
private readonly neuron: Neuron;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
id: string,
|
|
63
|
+
type: string,
|
|
64
|
+
params: ParameterInterface[],
|
|
65
|
+
options: ActionOptions,
|
|
66
|
+
neuron: Neuron,
|
|
67
|
+
) {
|
|
68
|
+
this.id = id;
|
|
69
|
+
this.type = type;
|
|
70
|
+
this.params = params;
|
|
71
|
+
this.options = options;
|
|
72
|
+
this.neuron = neuron;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
execute(context: ExecutionContext): ExecutionResult<string | null> {
|
|
76
|
+
const decisionParam = this.params.find((param) => param.name === "decision");
|
|
77
|
+
if (!decisionParam) {
|
|
78
|
+
return new ExecutionResult(false, context, null, ["Missing decision parameter"]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ParamCtor = this.neuron.getParameter(decisionParam.type);
|
|
82
|
+
const decision = ParamCtor
|
|
83
|
+
? new ParamCtor(decisionParam.id, decisionParam.type, decisionParam.name, decisionParam.value, decisionParam.options, decisionParam.defaultValue).getValue(context)
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
if (typeof decision !== "string") {
|
|
87
|
+
return new ExecutionResult(false, context, null, ["Invalid decision value"]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const nextContext: ExecutionContext = {
|
|
91
|
+
...context,
|
|
92
|
+
messages: [
|
|
93
|
+
...context.messages,
|
|
94
|
+
{ type: MessageType.INFO, text: `Eligibility decision: ${decision}` },
|
|
95
|
+
],
|
|
96
|
+
state: {
|
|
97
|
+
...context.state,
|
|
98
|
+
eligibility: { eligible: decision === "approved", decision },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return new ExecutionResult(true, nextContext, decision);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const neuron = new Neuron();
|
|
107
|
+
neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter);
|
|
108
|
+
neuron.registerAction(SetDecisionAction.TYPE, SetDecisionAction);
|
|
109
|
+
|
|
110
|
+
const result = new Synapse(neuron).execute(script, input as ExecutionContext);
|
|
111
|
+
const eligibility = result.context.state.eligibility as
|
|
112
|
+
| { eligible?: boolean; decision?: string }
|
|
113
|
+
| undefined;
|
|
114
|
+
const actual = {
|
|
115
|
+
ok: result.isSuccessful(),
|
|
116
|
+
rulesExecuted: result.value,
|
|
117
|
+
eligible: eligibility?.eligible,
|
|
118
|
+
decision: eligibility?.decision,
|
|
119
|
+
messages: result.context.messages.map((message) => message.text),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (JSON.stringify(actual) !== JSON.stringify(expectedOutput)) {
|
|
123
|
+
console.error(JSON.stringify({ expected: expectedOutput, actual }, null, 2));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(JSON.stringify(actual, null, 2));
|
|
128
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Pricing Rules Example
|
|
2
|
+
|
|
3
|
+
Run a pricing decision from serializable JSON. The rule checks the cart subtotal from `input.json`, applies a VIP discount from `rules.json`, and verifies the final context against `expected-output.json`.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
From the repository root:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
yarn build
|
|
11
|
+
node examples/pricing-rules/run.ts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Expected summary:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"ok": true,
|
|
19
|
+
"rulesExecuted": 1,
|
|
20
|
+
"finalTotal": 105,
|
|
21
|
+
"discountAmount": 20,
|
|
22
|
+
"messages": ["Applied 16% discount: -20"]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Files
|
|
27
|
+
|
|
28
|
+
- `rules.json` — the serializable Neuron-JS script.
|
|
29
|
+
- `input.json` — the execution context used by the script.
|
|
30
|
+
- `expected-output.json` — the checked output summary.
|
|
31
|
+
- `run.ts` — registers the example vocabulary, executes the script, and fails if output differs.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pricing-rules-demo",
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "vip-order-discount",
|
|
6
|
+
"type": "simple_rule",
|
|
7
|
+
"options": {},
|
|
8
|
+
"conditions": [
|
|
9
|
+
{
|
|
10
|
+
"id": "minimum-cart-subtotal",
|
|
11
|
+
"type": "compare_two_numbers",
|
|
12
|
+
"options": {},
|
|
13
|
+
"params": [
|
|
14
|
+
{ "id": "cart-subtotal", "name": "op1", "type": "state_number", "value": "cart.subtotal", "options": {} },
|
|
15
|
+
{ "id": "comparison", "name": "comp", "type": "comparator", "value": ">=", "options": {} },
|
|
16
|
+
{ "id": "discount-threshold", "name": "op2", "type": "simple_number", "value": "100", "options": {} }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"actions": [
|
|
21
|
+
{
|
|
22
|
+
"id": "apply-vip-discount",
|
|
23
|
+
"type": "apply_discount",
|
|
24
|
+
"options": {},
|
|
25
|
+
"params": [
|
|
26
|
+
{ "id": "discount-percent", "name": "percent", "type": "simple_number", "value": "16", "options": {} }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionResult,
|
|
3
|
+
MessageType,
|
|
4
|
+
Neuron,
|
|
5
|
+
Synapse,
|
|
6
|
+
type ActionOptions,
|
|
7
|
+
type ExecutionContext,
|
|
8
|
+
type ParameterInterface,
|
|
9
|
+
} from "../../dist/esm/index.js";
|
|
10
|
+
import expectedOutput from "./expected-output.json" with { type: "json" };
|
|
11
|
+
import input from "./input.json" with { type: "json" };
|
|
12
|
+
import script from "./rules.json" with { type: "json" };
|
|
13
|
+
|
|
14
|
+
function readStatePath(context: ExecutionContext, path: string): unknown {
|
|
15
|
+
return path.split(".").reduce<unknown>((current, segment) => {
|
|
16
|
+
if (current && typeof current === "object" && segment in current) {
|
|
17
|
+
return (current as Record<string, unknown>)[segment];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}, context.state);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class StateNumberParameter {
|
|
24
|
+
static readonly TYPE = "state_number";
|
|
25
|
+
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly value: string;
|
|
30
|
+
readonly options: Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
id: string,
|
|
34
|
+
type: string,
|
|
35
|
+
name: string,
|
|
36
|
+
value: string,
|
|
37
|
+
options: Record<string, unknown>,
|
|
38
|
+
) {
|
|
39
|
+
this.id = id;
|
|
40
|
+
this.type = type;
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.value = value;
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getValue(context: ExecutionContext): number | null {
|
|
47
|
+
const value = readStatePath(context, this.value);
|
|
48
|
+
return typeof value === "number" ? value : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ApplyDiscountAction {
|
|
53
|
+
static readonly TYPE = "apply_discount";
|
|
54
|
+
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly type: string;
|
|
57
|
+
private readonly params: ParameterInterface[];
|
|
58
|
+
readonly options: ActionOptions;
|
|
59
|
+
private readonly neuron: Neuron;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
id: string,
|
|
63
|
+
type: string,
|
|
64
|
+
params: ParameterInterface[],
|
|
65
|
+
options: ActionOptions,
|
|
66
|
+
neuron: Neuron,
|
|
67
|
+
) {
|
|
68
|
+
this.id = id;
|
|
69
|
+
this.type = type;
|
|
70
|
+
this.params = params;
|
|
71
|
+
this.options = options;
|
|
72
|
+
this.neuron = neuron;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
execute(context: ExecutionContext): ExecutionResult<number | null> {
|
|
76
|
+
const percentParam = this.params.find((param) => param.name === "percent");
|
|
77
|
+
if (!percentParam) {
|
|
78
|
+
return new ExecutionResult(false, context, null, ["Missing percent parameter"]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ParamCtor = this.neuron.getParameter(percentParam.type);
|
|
82
|
+
const percent = ParamCtor
|
|
83
|
+
? new ParamCtor(percentParam.id, percentParam.type, percentParam.name, percentParam.value, percentParam.options, percentParam.defaultValue).getValue(context)
|
|
84
|
+
: null;
|
|
85
|
+
const subtotal = readStatePath(context, "cart.subtotal");
|
|
86
|
+
|
|
87
|
+
if (typeof subtotal !== "number" || typeof percent !== "number") {
|
|
88
|
+
return new ExecutionResult(false, context, null, ["Invalid discount input"]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const discountAmount = Math.round(subtotal * (percent / 100));
|
|
92
|
+
const finalTotal = subtotal - discountAmount;
|
|
93
|
+
const nextContext: ExecutionContext = {
|
|
94
|
+
...context,
|
|
95
|
+
messages: [
|
|
96
|
+
...context.messages,
|
|
97
|
+
{ type: MessageType.INFO, text: `Applied ${percent}% discount: -${discountAmount}` },
|
|
98
|
+
],
|
|
99
|
+
state: {
|
|
100
|
+
...context.state,
|
|
101
|
+
cart: {
|
|
102
|
+
...(context.state.cart as Record<string, unknown>),
|
|
103
|
+
discountPercent: percent,
|
|
104
|
+
discountAmount,
|
|
105
|
+
finalTotal,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return new ExecutionResult(true, nextContext, finalTotal);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const neuron = new Neuron();
|
|
115
|
+
neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter);
|
|
116
|
+
neuron.registerAction(ApplyDiscountAction.TYPE, ApplyDiscountAction);
|
|
117
|
+
|
|
118
|
+
const result = new Synapse(neuron).execute(script, input as ExecutionContext);
|
|
119
|
+
const cart = result.context.state.cart as
|
|
120
|
+
| { finalTotal?: number; discountAmount?: number }
|
|
121
|
+
| undefined;
|
|
122
|
+
const actual = {
|
|
123
|
+
ok: result.isSuccessful(),
|
|
124
|
+
rulesExecuted: result.value,
|
|
125
|
+
finalTotal: cart?.finalTotal,
|
|
126
|
+
discountAmount: cart?.discountAmount,
|
|
127
|
+
messages: result.context.messages.map((message) => message.text),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (JSON.stringify(actual) !== JSON.stringify(expectedOutput)) {
|
|
131
|
+
console.error(JSON.stringify({ expected: expectedOutput, actual }, null, 2));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(JSON.stringify(actual, null, 2));
|
|
136
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Workflow Routing Example
|
|
2
|
+
|
|
3
|
+
Run a deterministic workflow-routing decision from JSON. The rule checks ticket priority from `input.json` and routes high-priority work to the escalation lane.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
From the repository root:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
yarn build
|
|
11
|
+
node examples/workflow-routing/run.ts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Expected summary:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"ok": true,
|
|
19
|
+
"rulesExecuted": 1,
|
|
20
|
+
"route": "escalation",
|
|
21
|
+
"slaHours": 4,
|
|
22
|
+
"messages": ["Workflow route: escalation within 4h"]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Files
|
|
27
|
+
|
|
28
|
+
- `rules.json` — the serializable Neuron-JS script.
|
|
29
|
+
- `input.json` — the execution context used by the script.
|
|
30
|
+
- `expected-output.json` — the checked output summary.
|
|
31
|
+
- `run.ts` — registers the example vocabulary, executes the script, and fails if output differs.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "workflow-routing-demo",
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "route-critical-ticket",
|
|
6
|
+
"type": "simple_rule",
|
|
7
|
+
"options": {},
|
|
8
|
+
"conditions": [
|
|
9
|
+
{
|
|
10
|
+
"id": "priority-threshold",
|
|
11
|
+
"type": "compare_two_numbers",
|
|
12
|
+
"options": {},
|
|
13
|
+
"params": [
|
|
14
|
+
{ "id": "ticket-priority", "name": "op1", "type": "state_number", "value": "ticket.priority", "options": {} },
|
|
15
|
+
{ "id": "comparison", "name": "comp", "type": "comparator", "value": ">=", "options": {} },
|
|
16
|
+
{ "id": "critical-threshold", "name": "op2", "type": "simple_number", "value": "8", "options": {} }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"actions": [
|
|
21
|
+
{
|
|
22
|
+
"id": "assign-escalation-route",
|
|
23
|
+
"type": "set_route",
|
|
24
|
+
"options": {},
|
|
25
|
+
"params": [
|
|
26
|
+
{ "id": "route-name", "name": "route", "type": "simple_string", "value": "escalation", "options": {} },
|
|
27
|
+
{ "id": "sla-hours", "name": "slaHours", "type": "simple_number", "value": "4", "options": {} }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionResult,
|
|
3
|
+
MessageType,
|
|
4
|
+
Neuron,
|
|
5
|
+
Synapse,
|
|
6
|
+
type ActionOptions,
|
|
7
|
+
type ExecutionContext,
|
|
8
|
+
type ParameterInterface,
|
|
9
|
+
} from "../../dist/esm/index.js";
|
|
10
|
+
import expectedOutput from "./expected-output.json" with { type: "json" };
|
|
11
|
+
import input from "./input.json" with { type: "json" };
|
|
12
|
+
import script from "./rules.json" with { type: "json" };
|
|
13
|
+
|
|
14
|
+
function readStatePath(context: ExecutionContext, path: string): unknown {
|
|
15
|
+
return path.split(".").reduce<unknown>((current, segment) => {
|
|
16
|
+
if (current && typeof current === "object" && segment in current) {
|
|
17
|
+
return (current as Record<string, unknown>)[segment];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}, context.state);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class StateNumberParameter {
|
|
24
|
+
static readonly TYPE = "state_number";
|
|
25
|
+
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly value: string;
|
|
30
|
+
readonly options: Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
id: string,
|
|
34
|
+
type: string,
|
|
35
|
+
name: string,
|
|
36
|
+
value: string,
|
|
37
|
+
options: Record<string, unknown>,
|
|
38
|
+
) {
|
|
39
|
+
this.id = id;
|
|
40
|
+
this.type = type;
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.value = value;
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getValue(context: ExecutionContext): number | null {
|
|
47
|
+
const value = readStatePath(context, this.value);
|
|
48
|
+
return typeof value === "number" ? value : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class SetRouteAction {
|
|
53
|
+
static readonly TYPE = "set_route";
|
|
54
|
+
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly type: string;
|
|
57
|
+
private readonly params: ParameterInterface[];
|
|
58
|
+
readonly options: ActionOptions;
|
|
59
|
+
private readonly neuron: Neuron;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
id: string,
|
|
63
|
+
type: string,
|
|
64
|
+
params: ParameterInterface[],
|
|
65
|
+
options: ActionOptions,
|
|
66
|
+
neuron: Neuron,
|
|
67
|
+
) {
|
|
68
|
+
this.id = id;
|
|
69
|
+
this.type = type;
|
|
70
|
+
this.params = params;
|
|
71
|
+
this.options = options;
|
|
72
|
+
this.neuron = neuron;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private resolveParam(context: ExecutionContext, name: string): unknown {
|
|
76
|
+
const param = this.params.find((item) => item.name === name);
|
|
77
|
+
if (!param) return null;
|
|
78
|
+
const ParamCtor = this.neuron.getParameter(param.type);
|
|
79
|
+
return ParamCtor
|
|
80
|
+
? new ParamCtor(param.id, param.type, param.name, param.value, param.options, param.defaultValue).getValue(context)
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
execute(context: ExecutionContext): ExecutionResult<string | null> {
|
|
85
|
+
const route = this.resolveParam(context, "route");
|
|
86
|
+
const slaHours = this.resolveParam(context, "slaHours");
|
|
87
|
+
|
|
88
|
+
if (typeof route !== "string" || typeof slaHours !== "number") {
|
|
89
|
+
return new ExecutionResult(false, context, null, ["Invalid route input"]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextContext: ExecutionContext = {
|
|
93
|
+
...context,
|
|
94
|
+
messages: [
|
|
95
|
+
...context.messages,
|
|
96
|
+
{ type: MessageType.INFO, text: `Workflow route: ${route} within ${slaHours}h` },
|
|
97
|
+
],
|
|
98
|
+
state: {
|
|
99
|
+
...context.state,
|
|
100
|
+
workflow: { route, slaHours },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return new ExecutionResult(true, nextContext, route);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const neuron = new Neuron();
|
|
109
|
+
neuron.registerParameter(StateNumberParameter.TYPE, StateNumberParameter);
|
|
110
|
+
neuron.registerAction(SetRouteAction.TYPE, SetRouteAction);
|
|
111
|
+
|
|
112
|
+
const result = new Synapse(neuron).execute(script, input as ExecutionContext);
|
|
113
|
+
const workflow = result.context.state.workflow as
|
|
114
|
+
| { route?: string; slaHours?: number }
|
|
115
|
+
| undefined;
|
|
116
|
+
const actual = {
|
|
117
|
+
ok: result.isSuccessful(),
|
|
118
|
+
rulesExecuted: result.value,
|
|
119
|
+
route: workflow?.route,
|
|
120
|
+
slaHours: workflow?.slaHours,
|
|
121
|
+
messages: result.context.messages.map((message) => message.text),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (JSON.stringify(actual) !== JSON.stringify(expectedOutput)) {
|
|
125
|
+
console.error(JSON.stringify({ expected: expectedOutput, actual }, null, 2));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(JSON.stringify(actual, null, 2));
|
|
130
|
+
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebasoft/neuron-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "AI-friendly TypeScript rules engine for serializable JSON business rules and deterministic workflow decisions.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/SebaSOFT/neuron-js.git"
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"node": ">=24.0.0"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tshy",
|
|
16
|
+
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tshy",
|
|
17
17
|
"dev": "vitest",
|
|
18
18
|
"test": "vitest run",
|
|
19
|
+
"examples": "yarn build && node examples/pricing-rules/run.ts && node examples/eligibility-check/run.ts && node examples/workflow-routing/run.ts",
|
|
19
20
|
"lint": "biome check .",
|
|
20
21
|
"format": "biome format --write .",
|
|
21
22
|
"prepare": "yarn build",
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
"files": [
|
|
49
50
|
"dist",
|
|
50
51
|
"src",
|
|
52
|
+
"schemas",
|
|
53
|
+
"examples",
|
|
51
54
|
"!src/**/*.test.ts",
|
|
52
55
|
"README.md",
|
|
53
56
|
"LICENSE"
|
|
@@ -66,5 +69,29 @@
|
|
|
66
69
|
"default": "./dist/commonjs/index.js"
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
}
|
|
72
|
+
},
|
|
73
|
+
"keywords": [
|
|
74
|
+
"rules-engine",
|
|
75
|
+
"rule-engine",
|
|
76
|
+
"json-rules",
|
|
77
|
+
"business-rules",
|
|
78
|
+
"decision-engine",
|
|
79
|
+
"json-logic",
|
|
80
|
+
"workflow-automation",
|
|
81
|
+
"typescript",
|
|
82
|
+
"javascript",
|
|
83
|
+
"nodejs",
|
|
84
|
+
"browser",
|
|
85
|
+
"ai-agents",
|
|
86
|
+
"llm",
|
|
87
|
+
"mcp",
|
|
88
|
+
"deterministic",
|
|
89
|
+
"functional-programming",
|
|
90
|
+
"validation",
|
|
91
|
+
"feature-flags",
|
|
92
|
+
"policy-engine",
|
|
93
|
+
"authorization",
|
|
94
|
+
"n8n",
|
|
95
|
+
"langgraph"
|
|
96
|
+
]
|
|
70
97
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sebasoft.github.io/neuron-js/schemas/execution-context.schema.json",
|
|
4
|
+
"title": "Neuron-JS Execution Context",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"required": ["messages", "state"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"messages": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": ["type", "text"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"type": { "enum": ["debug", "info", "warn", "error"] },
|
|
16
|
+
"text": { "type": "string" }
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"state": { "type": "object", "additionalProperties": true }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://sebasoft.github.io/neuron-js/schemas/execution-output.schema.json",
|
|
4
|
+
"title": "Neuron-JS Execution Output",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": true,
|
|
7
|
+
"required": ["ok", "rulesExecuted", "messages"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"ok": { "type": "boolean" },
|
|
10
|
+
"rulesExecuted": { "type": ["number", "null"] },
|
|
11
|
+
"messages": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": { "type": "string" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|