New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

operational-decision-tree

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

operational-decision-tree - npm Package Compare versions

Comparing version 3.0.1 to 4.0.0

132

example/tree.json

@@ -1,78 +0,82 @@

{
"decisions": [
[{
"property": "age",
"operation": ">",
"value": 40
}]
],
"branches": [
{
"runOnHit": [
"howOld"
],
"decisions": [
[{
"property": "random",
"operation": "<",
"value": 30
}],
[{
"property": "random",
"operation": "<",
"value": 50
}],
[{
"property": "random",
"operation": "<",
"value": 90
}],
[{
"property": "random",
"operation": "<",
"value": 100
}]
],
"branches": [
{"result": "Leaf A: Aged under 40 and random less than 30"},
{"result": "Leaf B: Aged under 40 and random between 30 and 49"},
{"result": "Leaf C: Aged under 40 and random between 50 and 89"},
{"result": "Leaf D: Aged under 40 and random between 90 and 99"}
]
},
{
"decisions": [
[{
"branches": [{
"decisions": [{
"property": "age",
"operation": ">",
"value": 40
}],
"runOnHit":[],
"node": {
"branches": [{
"decisions":
[{
"property": "random",
"operation": "<",
"value": 30
}],
"runOnHit":["howOld"],
"node": [{"result": "Leaf A: Aged under 40 and random less than 30"}]
},
{
"decisions":
[{
"property": "random",
"operation": "<",
"value": 50
}],
"runOnHit":[],
"node": [{"result": "Leaf B: Aged under 40 and random between 30 and 49"}]
},
{
"decisions":
[{
"property": "random",
"operation": "<",
"value": 90
}],
"runOnHit":[],
"node": [{"result": "Leaf C: Aged under 40 and random between 50 and 89"}]
},
{
"decisions":
[{
"property": "random",
"operation": "<",
"value": 100
}],
"runOnHit":[],
"node": [{"result": "Leaf D: Aged under 40 and random between 90 and 99"}]
}
]}
},
{
"decisions": [{
"property": "nationality",
"operation": "in",
"value": ["US", "CA"]
}]
],
"branches": [
{
"decisions": [
[
{
}],
"node": {
"branches": [{
"decisions": [
{
"property": "age",
"operation": "<",
"value": 60
},
{
},
{
"property": "gender",
"operation": "==",
"value": "male"
}
]
}
],
"branches": [
{"result": "Leaf E: Over 60 or female, and not from US"},
{"result": "Leaf F: Male under 60 and not from US"}
]
},
{"result": "Leaf G: Over 40 and from US"}
]
"node": [{"result": "Leaf E: Over 60 or female, and not from US"}]
},
{
"node": [{"result": "Leaf G"}]
}]
}
]
}]
}

@@ -31,33 +31,42 @@

var resolvedIndex = 0
this.runHits(node, subjectData)
if (node.decisions) {
async.forEachOf(node.decisions, function (comparisons, index, callback) {
if (resolved) {
// run prepare for each decision as it is encountered
async.forEachOf(node.branches, function (nodeBranch, index, callback) {
if (resolved) {
callback(null)
} else {
self.runHits(nodeBranch, subjectData)
self.prepareSubject(nodeCount, node, nodeBranch, subjectData, populated, function (err) {
if (err) return cb(err)
var result = self.runDecisions(nodeCount, nodeBranch.decisions, subjectData, populated)
if (result) {
resolved = true
} else {
resolvedIndex++
}
callback(null)
})
}
}, function (err) {
if (err) return cb(err)
var nextNode = node.branches[resolvedIndex]
if (nextNode.node) {
if (nextNode.node.branches) {
self.runNode(nodeCount, nextNode.node, subjectData, populated, cb)
} else {
self.prepareSubject(nodeCount, node, node.decisions, comparisons, subjectData, populated, function (err) {
if (err) return cb(err)
var result = self.runComparisons(nodeCount, comparisons, subjectData, populated)
if (result) {
resolved = true
} else {
resolvedIndex++
}
callback(null)
})
// we have a final node
return cb(null, nextNode.node)
}
}, function (err) {
if (err) return cb(err)
self.handleDecision(nodeCount, node, subjectData, populated, resolvedIndex, cb)
})
} else {
cb(null, node, populated)
}
} else {
// we have a final node
return cb(null, nextNode)
}
})
}
DecisionTree.prototype.runComparisons = function (nodeCount, comparisons, subjectData, populated) {
DecisionTree.prototype.runDecisions = function (nodeCount, decisions, subjectData, populated) {
var self = this
for (var i in comparisons) {
var comparison = comparisons[i]
var result = this.compare(nodeCount, comparison.property, comparison, subjectData, populated)
for (var i in decisions) {
var decision = decisions[i]
var result = this.compare(nodeCount, decision.property, decision, subjectData, populated)
if (!result) {

@@ -70,9 +79,9 @@ return false

DecisionTree.prototype.runHits = function (node, subjectData) {
DecisionTree.prototype.runHits = function (nodeBranch, subjectData) {
var self = this
if (node.runOnHit && this.opts.runOnHit) {
for (var i in node.runOnHit) {
var fn = node.runOnHit[i]
if (nodeBranch.runOnHit && this.opts.runOnHit) {
for (var i in nodeBranch.runOnHit) {
var fn = nodeBranch.runOnHit[i]
if (this.opts.runOnHit[fn]) {
this.opts.runOnHit[fn](node, subjectData)
this.opts.runOnHit[fn](nodeBranch, subjectData)
}

@@ -85,23 +94,27 @@ }

DecisionTree.prototype.prepareSubject = function (nodeCount, node, decisions, comparisons, subjectData, populated, cb) {
DecisionTree.prototype.prepareSubject = function (nodeCount, node, nodeBranch, subjectData, populated, cb) {
var self = this
async.each(comparisons, function (comparison, callback) {
if (populated && populated[nodeCount] && populated[nodeCount].hasOwnProperty(comparison.property)) {
callback(null)
} else if (self.opts.populateFunctions && self.opts.populateFunctions[comparison.property]) {
self.opts.populateFunctions[comparison.property](node, comparisons, comparison, subjectData, function (err, result) {
if (err) return cb(err)
if (!populated[nodeCount]) {
populated[nodeCount] = {}
}
populated[nodeCount][comparison.property] = result
if (nodeBranch.decisions) {
async.each(nodeBranch.decisions, function (decision, callback) {
if (populated && populated[nodeCount] && populated[nodeCount].hasOwnProperty(decision.property)) {
callback(null)
} else if (self.opts.populateFunctions && self.opts.populateFunctions[decision.property]) {
self.opts.populateFunctions[decision.property](node, nodeBranch, decision, subjectData, function (err, result) {
if (err) return cb(err)
if (!populated[nodeCount]) {
populated[nodeCount] = {}
}
populated[nodeCount][decision.property] = result
return callback(null)
})
} else {
return callback(null)
})
} else {
return callback(null)
}
}, function (err) {
if (err) return err
}
}, function (err) {
if (err) return err
return cb(null)
})
} else {
return cb(null)
})
}
}

@@ -119,3 +132,6 @@

case ">":
if (val > comparison.value) {
if (typeof(val) === 'undefined' || val === null) {
val = 0
}
if (Number(val) > Number(comparison.value)) {
result = 1;

@@ -125,3 +141,6 @@ }

case ">=":
if (val >= comparison.value) {
if (typeof(val) === 'undefined' || val === null) {
val = 0
}
if (Number(val) >= Number(comparison.value)) {
result = 1;

@@ -131,3 +150,6 @@ }

case "<":
if (val < comparison.value) {
if (typeof(val) === 'undefined' || val === null) {
val = 0
}
if (Number(val) < Number(comparison.value)) {
result = 1;

@@ -137,3 +159,6 @@ }

case "<=":
if (val <= comparison.value) {
if (typeof(val) === 'undefined' || val === null) {
val = 0
}
if (Number(val) <= Number(comparison.value)) {
result = 1;

@@ -185,18 +210,10 @@ }

}
console.log("COMPARING: "+val+" "+comparison.operation+" "+comparison.value)
return result
}
DecisionTree.prototype.handleDecision = function (nodeCount, node, subjectData, populated, resolvedIndex, cb) {
if (node.branches) {
if (!node.branches[resolvedIndex]) {
return cb("result branch does not exist")
}
this.runNode(nodeCount, node.branches[resolvedIndex], subjectData, populated, cb)
} else {
return cb('Node with decisions has no branches! nodeCount:'+nodeCount+' resolvedIndex:'+resolvedIndex)
}
}
// utility functions
/*
DecisionTree.prototype.countDecisions = function (treeData, finished) {

@@ -243,2 +260,3 @@ var decisionCount = {}

}
*/

@@ -245,0 +263,0 @@ DecisionTree.prototype.incrementDecisionCount = function (decisionCount, decisionName) {

{
"name": "operational-decision-tree",
"version": "3.0.1",
"version": "4.0.0",
"description": "A Decision Tree executor that uses decision modules to decide pathways. Can accept binary or arbitrary decision modules.",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -7,11 +7,53 @@ # Operational-Decision-Tree #

## What it's not ##
## What it is ##
ODT is not designed to be a machine learning decision tree. By all means though, use it how you want.
ODT is a decision tree executor designed to run decision trees that are human-built and managed. You give it a tree object and a subject object. It then runs the tree against the subject and passes back the leaf node along with any data generated along the way.
## What it is ##
Usage looks like this:
```js
var ODT = require('operational-decision-tree')
ODT is a decision tree executor designed to run decision trees that are human-built and managed. Hence, it has features targetted at reducing the total number of nodes needed to achieve the desired leaf nodes.
var treeData = require('./binary-tree.json')
For each condition, ODT compares the subject data to the tree data based on the "operation" parameter you pass in the tree. Supported operations are:
var person = {
name: "Bob",
age: 37,
nationality: 'US'
}
var DecisionTree = new ODT({
makeDecision: {},
runOnHit: {}
})
DecisionTree.run(treeData, person, function (err, resultBranchNode) {
if (err) console.error(err)
console.log("Result", resultBranchNode)
})
```
Each node can have the following properties:
```js
var node = {
"decisions": [], // the decisions that will be run for this node in order to produce an output branch
"branches": [], // each branch is another node
"runOnHit": [], // a list of function names to run when this node is hit.
"makeDecision": "" // the name of a function to run instead of the built in comparison function
}
```
For each node:
1. check if 'makeDecision' is set.
* true: run a function by this name from corresponding opts object
* false: go to #2
2. ODT runs each comparison in the list and:
* if all comparisons are true, then the branch with the same index as this decision is chosen.
* if the chosen branch has the 'decisions' property or the 'makeDecisions' property, then return to step 1 and use this branch as the node to test
* if not, then the top level callback is triggered with this branch node as the result
* if any are false, then return to step 2 and test the next decision
Once we hit a branch node that doesn't have either the 'decisions' property or the 'makeDecision' property, then the callback is triggered with the branch node as the result.
For each comparison, ODT compares the subject data to the tree data based on the "operation" parameter you pass in the tree. Supported operations are:
* > subject value is greater than tree comparison value

@@ -28,12 +70,4 @@ * >= subject value is greater than or equal to tree comparison value

Several things are required to use an ODT:
* The executor (this package)
* The tree data (provided by you)
* The decision maker (a default decision maker is included in this package, but you can insert your own if need be)
The number of decisions should always be one less than the number of branches. In the event that all decisions come back false, the last branch is chosen.
Depending on the tree data you provide, the executor supports any combination of the following:
* Binary conditions (conditions that output either true or false)
* Arbitrary conditions (conditions that output an integer. In other words, more than two outcome branchways are possible)
* Multi-condition nodes (a single decision node can run multiple conditions)
## Example ##

@@ -44,37 +78,80 @@

{
"condition": {
"name": "conditionAge",
"property": "age",
"comparison": {
"decisions": [
[{
"property": "age",
"operation": ">",
"value": 40
}
},
}]
],
"branches": [
{
"condition": {
"name": "conditionAge",
"property": "age",
"comparison": {
"operation": ">",
"value": 20
}
},
"runOnHit": [
"howOld"
],
"decisions": [
[{
"property": "random",
"operation": "<",
"value": 30
}],
[{
"property": "random",
"operation": "<",
"value": 50
}],
[{
"property": "random",
"operation": "<",
"value": 90
}]
],
"branches": [
{"result": "Leaf A: Aged between 0 and 20"},
{"result": "Leaf B: Aged between 21 and 39"}
{"result": "Leaf A: Aged under 40 and random less than 30"},
{"result": "Leaf B: Aged under 40 and random between 30 and 49"},
{"result": "Leaf C: Aged under 40 and random between 50 and 89"},
{"result": "Leaf D: Aged under 40 and random between 90 and 99"}
]
},
{
"condition": {
"name": "conditionCountry",
"property": "nationality",
"comparison": {
"decisions": [
[{
"property": "nationality",
"operation": "in",
"value": ["US", "CA"]
}]
],
"branches": [
{
"decisions": [
[
{
"property": "age",
"operation": "<",
"value": 60
},
{
"property": "gender",
"operation": "==",
"value": "male"
}
]
],
"branches": [
{"result": "Leaf E: Over 60 or female, and not from US"},
{"result": "Leaf F: Male under 60 and not from US"}
]
},
{
"makeDecision": "decideFinalBranch",
"decisions": [
[{
"property": "percentage"
}]
],
"branches": [
{"result": "Leaf G"},
{"result": "Leaf H"},
{"result": "Leaf I"}
]
}
},
"branches": [
{"result": "Leaf C: Over 40 and not from North America"},
{"result": "Leaf D: Over 40 and from the North America"}
]

@@ -86,24 +163,1 @@ }

```js
var ODT = require('operational-decision-tree')
var treeData = require('./binary-tree.json')
var person = {
name: "Bob",
age: 37,
nationality: 'US'
}
var DecisionTree = new ODT()
DecisionTree.run(treeData, person, function (err, result) {
if (err) console.error("ERROR", err)
console.log("RESULT", result)
})
```
## A Note About Multi-Condition Nodes ##
Multi-condition nodes are useful for reducing the number of nodes needed for specific outcomes. They work well with binary trees, since the decision maker can resolve them with an && operation. However, when paired with arbitrary conditions, you may run into some problems depending on how your arbitrary conditions are setup.

@@ -14,22 +14,50 @@

var subject = {
value: 1
target: 1
}
var condition = {
name: 'conditionBinary',
property: 'value',
comparison: {
operation: '>',
value: 0
}
var decisions = [{
property: 'target',
operation: '>',
value: 2
}]
var result = DT.runDecisions(0, decisions, subject, {})
t.equal(result, false, 'Binary condition returns false')
subject.target = 3
var result = DT.runDecisions(0, decisions, subject, {})
t.equal(result, true, 'Binary condition returns true')
t.end()
})
tape('Test Decision with Multiple Conditions', function (t) {
var DT = new DTS()
var subject = {
testFirst: 1,
testSecond: 1
}
DT.testCondition(condition, subject, function (err, result) {
t.equal(result, 1, 'Binary condition returns 1')
subject.value = 0
DT.testCondition(condition, subject, function (err, result) {
t.equal(result, 0, 'Binary condition returns 0')
t.end()
})
})
var decisions = [
{
property: 'testFirst',
operation: '>',
value: 0
},
{
property: 'testSecond',
operation: '>',
value: 2
}
]
var result = DT.runDecisions(0, decisions, subject, {})
t.equal(result, false, 'Multiple conditions return false')
subject.testSecond = 3
var result = DT.runDecisions(0, decisions, subject, {})
t.equal(result, true, 'Multiple conditions returns true')
t.end()
})

@@ -44,17 +72,27 @@

var condition = {
name: 'conditionArbitrary',
property: 'random',
comparisons: [
var node = {
branches: [
{
operation: '<',
value: 30
decisions: [{
property: 'random',
operation: '<',
value: 30
}],
node: 'first'
},
{
operation: '<',
value: 50
decisions: [{
property: 'random',
operation: '<',
value: 50
}],
node: 'second'
},
{
operation: '<',
value: 100
decisions: [{
property: 'random',
operation: '<',
value: 100
}],
node: 'third'
}

@@ -64,10 +102,10 @@ ]

DT.testCondition(condition, subject, function (err, result) {
t.equal(result, 2, 'Arbitrary condition returns 2')
DT.runNode(0, node, subject, {}, function (err, result) {
t.equal(result, 'third', 'Node returns third node')
subject.random = 35
DT.testCondition(condition, subject, function (err, result) {
t.equal(result, 1, 'Arbitrary condition returns 1')
DT.runNode(0, node, subject, {}, function (err, result) {
t.equal(result, 'second', 'Node returns second node')
subject.random = 5
DT.testCondition(condition, subject, function (err, result) {
t.equal(result, 0, 'Arbitrary condition returns 0')
DT.runNode(0, node, subject, {}, function (err, result) {
t.equal(result, 'first', 'Node returns first node')
t.end()

@@ -79,59 +117,49 @@ })

tape('Test Condition With Opts', function (t) {
var DT = new DTS()
var subject = {
somevalue: 1
}
var condition = {
name: 'conditionOpts',
property: 'somevalue',
comparison: {
operation: '<',
value: 2
}
}
DT.testCondition(condition, subject, function (err, result) {
t.ok(result, 'Condition with opts returns true')
subject.somevalue = 3
DT.testCondition(condition, subject, function (err, result) {
t.notOk(result, 'Condition with opts returns false')
t.end()
})
})
})
tape('Test runNode', function (t) {
tape('Test Deep Tree', function (t) {
var DT = new DTS()
var subject = {
age: 37
random: 29,
age: 16
}
var node = {
condition: {
name: "conditionAge",
property: 'age',
comparison: {
operation: ">",
value: 20
}
},
branches: [
{
result: "Leaf A: Aged under 20"
},
{
result: "Leaf B: Aged 21 or over"
}
]
{
decisions: [{
property: 'random',
operation: '<',
value: 30
}],
node: {
branches: [
{
decisions: [{
property: 'age',
operation: '<',
value: 18
}],
node: 'underage'
},
'adult'
]
}
},
{
decisions: [{
property: 'random',
operation: '<',
value: 50
}],
node: 'second'
}
]
}
DT.runNode(node, subject, function (err, result) {
t.ok(result, 'Leaf B: Aged 21 or over')
subject.age = 8
DT.runNode(node, subject, function (err, result) {
t.ok(result, 'Leaf A: Aged under 20')
DT.runNode(0, node, subject, {}, function (err, result) {
t.equal(result, 'underage', 'Returns deep node')
subject.age = 21
DT.runNode(0, node, subject, {}, function (err, result) {
t.equal(result, 'adult', 'Returns simple node')
t.end()

@@ -142,38 +170,2 @@ })

tape('Test defaultDecider', function (t) {
var DT = new DTS()
var result
result = DT.defaultDecider(true)
t.equal(result, 1, 'Decider properly handles "true"')
result = DT.defaultDecider(false)
t.equal(result, 0, 'Decider properly handles "false"')
result = DT.defaultDecider(1)
t.equal(result, 1, 'Decider properly handles "1"')
result = DT.defaultDecider(0)
t.equal(result, 0, 'Decider properly handles "0"')
result = DT.defaultDecider(2)
t.equal(result, 2, 'Decider properly handles "2"')
result = DT.defaultDecider('Oops')
t.equal(result instanceof Error, new Error() instanceof Error, 'Decider properly returns an error on strings')
t.end()
})
tape('Test countConditions', function (t) {
var DT = new DTS()
DT.countConditions(sampleTree, function(err, conditionCount) {
t.equal(1, conditionCount.conditionPercent, 'countConditions returned the correct number of "conditionPercent"')
t.equal(3, conditionCount.conditionAsync, 'countConditions returned the correct number of "conditionAsync"')
t.end()
})
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc