├── .gitignore ├── .jshintrc ├── .npmignore ├── .project ├── .settings ├── .jsdtscope ├── com.eclipsesource.jshint.ui.prefs ├── org.eclipse.wst.jsdt.core.prefs ├── org.eclipse.wst.jsdt.ui.superType.container └── org.eclipse.wst.jsdt.ui.superType.name ├── .tern-project ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── infernal-engine.js └── infernal-engine.js.map ├── docs └── KnowledgeIFPschool.pdf ├── jsdoc └── conf.json ├── lib ├── Fact.js ├── RuleContext.js ├── index.js └── infernalUtils.js ├── package-lock.json ├── package.json ├── test ├── cli │ └── tracing.js ├── models │ ├── carModel.js │ └── critterModel.js └── test-engine.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Swap files 31 | *.swp 32 | *.swo 33 | 34 | #some undesired files 35 | zeros.txt 36 | 37 | #documentation 38 | doc 39 | out 40 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Nodeclipse Configuration v0.15.1 4 | // Strict Edition with some relaxations and switch to Node.js, no `use strict` 5 | // by Ory Band, Michael Haschke, Paul Verest 6 | // https://github.com/Nodeclipse/nodeclipse-1/blob/master/org.nodeclipse.ui/templates/common-templates/.jshintrc 7 | // JSHint Documentation is at http://www.jshint.com/docs/options/ 8 | // JSHint Integration v0.9.9 comes with JSHInt 2.1.10 , see https://github.com/eclipsesource/jshint-eclipse 9 | // Known issues: 10 | // newer JSHint can't be used https://github.com/eclipsesource/jshint-eclipse/issues/75 that depends on JSHint issues. 11 | // -------------------------------------------------------------------- 12 | // from https://gist.github.com/haschek/2595796 13 | // 14 | // @author http://michael.haschke.biz/ 15 | // @license http://unlicense.org/ 16 | // 17 | // This is a options template for [JSHint][1], using [JSHint example][2] 18 | // and [Ory Band's example][3] as basis and setting config values to 19 | // be most strict: 20 | // 21 | // * set all enforcing options to true 22 | // * set all relaxing options to false 23 | // * set all environment options to false, except the node value 24 | // * set all JSLint legacy options to false 25 | // 26 | // [1]: http://www.jshint.com/ 27 | // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json //404 28 | // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc //404 29 | // [4]: http://www.jshint.com/options/ 30 | 31 | // == Enforcing Options =============================================== 32 | // 33 | // These options tell JSHint to be more strict towards your code. Use 34 | // them if you want to allow only a safe subset of JavaScript, very 35 | // useful when your codebase is shared with a big number of developers 36 | // with different skill levels. Was all true. 37 | 38 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 39 | "curly" : true, // Require {} for every new block or scope. 40 | "eqeqeq" : true, // Require triple equals i.e. `===`. 41 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 42 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 43 | "latedef" : true, // Prohibit variable use before definition. 44 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 45 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 46 | "noempty" : true, // Prohibit use of empty blocks. 47 | "nonew" : true, // Prohibit use of constructors for side-effects. 48 | "plusplus" : false, // Prohibit use of `++` & `--`. //coding style related only 49 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 50 | "undef" : true, // Require all non-global variables be declared before they are used. 51 | "strict" : false, // Require `use strict` pragma in every file. 52 | "trailing" : true, // Prohibit trailing whitespaces. 53 | 54 | // == Relaxing Options ================================================ 55 | // 56 | // These options allow you to suppress certain types of warnings. Use 57 | // them only if you are absolutely positive that you know what you are 58 | // doing. Was all false. 59 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 60 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 61 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 62 | "eqnull" : false, // Tolerate use of `== null`. 63 | "es5" : false, // Allow EcmaScript 5 syntax. // es5 is default https://github.com/jshint/jshint/issues/1411 64 | "esnext" : false, // Allow ES.next (ECMAScript 6) specific features such as `const` and `let`. 65 | "evil" : false, // Tolerate use of `eval`. 66 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 67 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 68 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 69 | "iterator" : false, // Allow usage of __iterator__ property. 70 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 71 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 72 | "laxcomma" : true, // Suppress warnings about comma-first coding style. 73 | "loopfunc" : false, // Allow functions to be defined within loops. 74 | "maxerr" : 100, // This options allows you to set the maximum amount of warnings JSHint will produce before giving up. Default is 50. 75 | "moz" : false, // This options tells JSHint that your code uses Mozilla JavaScript extensions. Unless you develop specifically for the Firefox web browser you don't need this option. 76 | "multistr" : false, // Tolerate multi-line strings. 77 | "onecase" : false, // Tolerate switches with just one case. 78 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 79 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 80 | "scripturl" : false, // Tolerate script-targeted URLs. 81 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 82 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 83 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 84 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 85 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 86 | 87 | // == Environments ==================================================== 88 | // 89 | // These options pre-define global variables that are exposed by 90 | // popular JavaScript libraries and runtime environments—such as 91 | // browser or node.js. TODO JSHint Documentation has more, but it is not clear since what JSHint version they appeared 92 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 93 | "couch" : false, // Enable globals exposed by CouchDB. 94 | "devel" : false, // Allow development statements e.g. `console.log();`. 95 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 96 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 97 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 98 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 99 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 100 | "phantom" : false, //?since version? This option defines globals available when your core is running inside of the PhantomJS runtime environment. 101 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 102 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 103 | "worker" : false, //?since version? This option defines globals available when your code is running inside of a Web Worker. 104 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 105 | "yui" : false, //?since version? This option defines globals exposed by the YUI JavaScript framework. 106 | 107 | // == JSLint Legacy =================================================== 108 | // 109 | // These options are legacy from JSLint. Aside from bug fixes they will 110 | // not be improved in any way and might be removed at any point. 111 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 112 | "onevar" : false, // Allow only one `var` statement per function. 113 | "passfail" : false, // Stop on first error. 114 | "white" : false, // Check against strict whitespace and indentation rules. 115 | 116 | // == Undocumented Options ============================================ 117 | // 118 | // While Michael have found these options in [example1][2] and [example2][3] (already gone 404) 119 | // they are not described in the [JSHint Options documentation][4]. 120 | 121 | "predef" : [ // Extra globals. 122 | //"exampleVar", 123 | //"anotherCoolGlobal", 124 | //"iLoveDouglas" 125 | "Java", "JavaFX", "$ARG" //no effect 126 | ] 127 | //, "indent" : 2 // Specify indentation spacing 128 | } 129 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .settings 3 | .gitignore 4 | .jshintrc 5 | .project 6 | .tern-project 7 | .travis.yml 8 | test 9 | tmp 10 | doc 11 | dist 12 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | infernal-engine 4 | 5 | 6 | 7 | 8 | 9 | com.eclipsesource.jshint.ui.builder 10 | 11 | 12 | 13 | 14 | 15 | org.nodeclipse.ui.NodeNature 16 | org.eclipse.wst.jsdt.core.jsNature 17 | tern.eclipse.ide.core.ternnature 18 | 19 | 20 | -------------------------------------------------------------------------------- /.settings/.jsdtscope: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.settings/com.eclipsesource.jshint.ui.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | excluded=//*.json\:bower_components//*\:node_lib//*\:node_modules//* 3 | included=//*.jjs\://*.js\://*.jshintrc\://*.mjs\://*.njs\://*.pjs\://*.vjs 4 | projectSpecificOptions=true 5 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.jsdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | semanticValidation=disabled 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.jsdt.ui.superType.container: -------------------------------------------------------------------------------- 1 | org.eclipse.wst.jsdt.launching.JRE_CONTAINER -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.jsdt.ui.superType.name: -------------------------------------------------------------------------------- 1 | Global -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | {"libs":["ecma5"],"plugins":{"node":{}},"ide":{}} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jean-Philippe Gravel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infernal Engine # 2 | 3 | [![Join the chat at https://gitter.im/formix/infernal-engine](https://badges.gitter.im/formix/infernal-engine.svg)](https://gitter.im/formix/infernal-engine?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/formix/infernal-engine.svg?branch=master)](https://travis-ci.org/formix/infernal-engine) 5 | 6 | The **Infernal Engine** is a 1-order logic forward chaining inference 7 | engine. Inference engine are used to build expert systems to modelise human 8 | experience using rules. The goal of the engine is to maintain its internal 9 | fact base concistent with its rules base. Each change to the fact or rule base 10 | triggers the inference. It is possible to control how and when the inference 11 | is fired using *models*. 12 | 13 | [InfernalEngine class Reference](https://formix.github.io/infernal-engine/) 14 | 15 | See [Philippe Morignot order logic explanations](http://philippe.morignot.free.fr/Articles/KnowledgeIFPschool.pdf) 16 | to know more about different inference engine order logics. 17 | 18 | ## Usage ## 19 | 20 | ### Using The "Raw" Engine ### 21 | 22 | ```javascript 23 | var InfernalEngine = require("infernal-engine"); 24 | let engine = new InfernalEngine(); 25 | 26 | await engine.def("count5", async function(i) { 27 | if (typeof i !== "undefined" && i < 5) { 28 | return { "i": i + 1 }; 29 | } 30 | }); 31 | 32 | await engine.assert("i", 1); 33 | 34 | let final_i = await engine.peek("i"); 35 | 36 | console.log(final_i); // displays 5 37 | ``` 38 | 39 | Some things to consider for the above example: 40 | 41 | 1. All **InfernalEngine** methods must be called with *await*. 42 | 43 | 2. Rules must be **async function**! the parser do not recognize lambda 44 | parameters pattern yet. 45 | 46 | 3. Defining a rule using the *#def* method triggers the inference for 47 | that rule. Therefore rules code must be resilient to undefined facts 48 | unless implemented within a *model* that has pre-initialized facts. 49 | 50 | 4. Rules must either return nothing or an object that maps a relative or 51 | absolute fact reference. Those concepts are explained in details below. 52 | 53 | 5. Asserting a fact is done using the *#assert* method. Asserting new facts 54 | triggers the inference as well. To retract a fact, use *#retract*. 55 | 56 | 6. If you want to know a fact value, call the *#peek* method. 57 | 58 | This simple example shows all the atomic inference methods of the 59 | **InfernalEngine**. You can go from there and have fun building a complex web 60 | of facts and rules already. But sincerely, your expert system programming 61 | life would be pretty miserable without the *model*. 62 | 63 | ### Rule ### 64 | 65 | Rules are simple Javascript functions. A rule is defined within a context and 66 | can reference facts within that context that match the function parameter 67 | names. For example, the rule at path "/speed/maxSpeedReached" is inside the 68 | context "/speed/". Given the following model: 69 | 70 | ```javascript 71 | { 72 | engine: "V6", 73 | speed: { 74 | max: 150, 75 | value: 0, 76 | maxReached: false, 77 | 78 | maxSpeedReached: async function(value, max) { 79 | return { "maxReached": value === max}; 80 | }, 81 | 82 | user: { 83 | input: "" 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | The rule parameters `value` and `max` will respectively reference 90 | "/speed/value" and "/speed/max". But how can we reference a fact that is 91 | outside the rule context? Simple, by using the parameter annotation comment 92 | like this: 93 | 94 | ```javascript 95 | { 96 | engine: "V6", 97 | speed: { 98 | max: 150, 99 | value: 0, 100 | maxReached: false, 101 | 102 | maxSpeedReached: async function(value, max) { 103 | return { "maxReached": value === max}; 104 | }, 105 | 106 | translate: function(/*@ user/input */ input) { 107 | return {"value": Number(input)} 108 | } 109 | 110 | user: { 111 | input: "", 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | Annotation comments must be placed in front of the annotated parameter. 118 | 119 | #### Return Object #### 120 | 121 | Rules must return an object or nothing. The simple case is to return an object 122 | where the key is the fact to update and the value being the valu to assign to 123 | that fact. You have plenty of examples above. 124 | 125 | What we just explained is a shorthand for the more general case though. The 126 | equivalent general case would be to return the command '#assert' instead of the 127 | factpath-value pair. This is the command list available to be returned by a 128 | rule: 129 | 130 | 1. "#assert": {path:str, value:any} 131 | 132 | 2. "#retract": {path:str} 133 | 134 | 3. "#def": {path:str, value:AsyncFunction} 135 | 136 | 4. "#undef": {path:str} 137 | 138 | 5. "#import": {path:str, value:object} 139 | 140 | Each command expects an object with one or two properties which are 'path' 141 | and 'value'. Example: 142 | 143 | ```javascript 144 | await engine.def("count5", async function(i) { 145 | if (typeof i !== "undefined" && i < 5) { 146 | return { 147 | "#assert": { 148 | path: "i", 149 | value: i + 1 150 | } 151 | }; 152 | } 153 | }); 154 | ``` 155 | 156 | The shorthand structure works with scalar values and arrays (#assert), 157 | functions (#def) and objects (#import). Thus returning the following 158 | structure: 159 | 160 | ```javascript 161 | // This rule will execute only once when defined. 162 | await engine.def("assert_def_import", async function() { 163 | let model = require("./models/someInferenceModel"); 164 | return { 165 | "/input/quantity": "23.5", // #assert 166 | "/input/isQuantityNumeric": async function(/*@ /input/quantity */ quantity) { // #def 167 | if (Number(quantity) === NaN) { 168 | return { 169 | message: "Invalid input quantity." 170 | }; 171 | } 172 | }, 173 | "/submodel": model // #import 174 | }; 175 | }); 176 | ``` 177 | 178 | ### Defining a Model ### 179 | 180 | A model is a way to define rules and facts using a simple Javascript object. 181 | The following code block translates the Wikipedia article example for 182 | [forward chaining inference](https://en.wikipedia.org/wiki/Forward\_chaining) 183 | into an InfernalEngine *model* representation. 184 | 185 | #### Model Example #### 186 | 187 | ```javascript 188 | var critterModel = { 189 | 190 | name: "Fritz", 191 | 192 | sound: "", 193 | eats: "", 194 | sings: false, 195 | 196 | color: "unknown", 197 | species: "unknown", 198 | 199 | isFrog: async function(sound, eats){ 200 | let species = "unknown"; 201 | if (sound === "croaks" && eats === "flies") { 202 | species = "frog"; 203 | } 204 | return {"species": species}; 205 | }, 206 | 207 | isCanary: async function(sound, sings) { 208 | if ( sings && sound === "chirps" ) { 209 | return {"species": "canary"}; 210 | } 211 | }, 212 | 213 | isGreen: async function(species) { 214 | if (species === "frog") { 215 | return {"color": "green"}; 216 | } 217 | }, 218 | 219 | isYellow: async function(species) { 220 | if (species === "canary") { 221 | return {"color": "yellow"}; 222 | } 223 | } 224 | 225 | }; 226 | ``` 227 | 228 | #### Importing a Model #### 229 | 230 | A model is a javascript object with scalar values or arrays and methods. 231 | Facts and rules paths are built based on their location in the object. 232 | The internal representation of a model is explained in the next section. 233 | 234 | The following example displays the critter model after setting two facts at 235 | the same time: 236 | 237 | ```javascript 238 | let InfernalEngine = require("infernal-engine"); 239 | let engine = new InfernalEngine(); 240 | 241 | let critterModel = require("./models/critterModel"); 242 | 243 | await engine.import(critterModel); 244 | await engine.import({ 245 | eats: "flies", 246 | sound: "croaks" 247 | }); 248 | 249 | let state = await engine.export(); 250 | console.log(JSON.stringify(state, null, " ")); 251 | ``` 252 | 253 | #### Model structure output #### 254 | 255 | ```json 256 | { 257 | "name": "Fritz", 258 | "sound": "croaks", 259 | "eats": "flies", 260 | "species": "frog", 261 | "color": "green" 262 | } 263 | ``` 264 | 265 | In the above example, the inference runs twice: once after the model is 266 | imported and another time after the facts `eats` and `sound` are imported. 267 | 268 | Given the set of rules, the inference that runs after importing the 269 | `critterModel` should not change anything to the internal fact base. On 270 | another hand, inferring after importing `eats` and `sound` facts will update 271 | the internal fact base. 272 | 273 | This example shows how to set multiple facts at once and have the inferrence 274 | being triggered only once. Using the `#assert` method for each fact would have 275 | worked as well but it would have launched the inference one more time. 276 | 277 | It is also possible to import a model into a different path if the import 278 | method `path` parameter was given. For example: 279 | 280 | ```javascript 281 | await engine.import(critterModel, "/some/other/path"); 282 | ``` 283 | 284 | Doing so would have resulted in the following JSON output: 285 | 286 | ```json 287 | { 288 | "some": { 289 | "other": { 290 | "path" : { 291 | "name": "Fritz", 292 | "sound": "croaks", 293 | "eats": "flies", 294 | "species": "frog", 295 | "color": "green" 296 | } 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | Finally, the `export` method also supports the path parameter. When provided, 303 | the exported model only contains facts that are under it. Given the previous 304 | `import` example with a path, exporting the content of `"/some/other/path"` 305 | would print the original output. Example: 306 | 307 | ```javascript 308 | let state = await engine.export("/some/other/path"); 309 | ``` 310 | 311 | Back to the original result: 312 | 313 | ```json 314 | { 315 | "name": "Fritz", 316 | "sound": "croaks", 317 | "eats": "flies", 318 | "species": "frog", 319 | "color": "green" 320 | } 321 | ``` 322 | 323 | ### How models are represented internally ### 324 | 325 | The `import` method crawls the model object structure and creates the matching 326 | fact and rule mapings. This is how the fact and rules are mapped in the engine 327 | based on the following model: 328 | 329 | ```javascript 330 | let carModel = { 331 | 332 | name: "Minivan", 333 | 334 | speed: { 335 | input: "0", 336 | limit: 140, 337 | value: 0, 338 | 339 | inputIsValidInteger: async function(input) { 340 | let isInt = /^-?\d+$/.test(input); 341 | if (!isInt) { 342 | return { "../message": `Error: '${input}' is not a valid integer.` } 343 | } 344 | return { 345 | "../message": undefined, 346 | value: Number(input) 347 | }; 348 | }, 349 | 350 | valueIsUnderLimit: async function(value, limit) { 351 | if (value > limit) { 352 | return { 353 | value: limit, 354 | "/message": `WARN: The speed input can not exceed the speed limit of ${limit}.` 355 | } 356 | } 357 | } 358 | 359 | } 360 | } 361 | ``` 362 | 363 | #### Facts #### 364 | 365 | | Name | Value | 366 | |:------------------ |:---------:| 367 | | "/name" | "Minivan" | 368 | | "/speed/input" | "0" | 369 | | "/speed/limit" | 140 | 370 | | "/speed/value" | 0 | 371 | 372 | #### Rules #### 373 | 374 | | Name | Value | 375 | |:---------------------------- |:--------------:| 376 | | "/speed/inputIsValidInteger" | *\[function\]* | 377 | | "/speed/valueIsUnderLimit" | *\[function\]* | 378 | 379 | By default rules parameters are fetching facts that have the same name as the 380 | given parameter from the same rule context. To reach a fact somewhere else in 381 | the fact tree, a parameter can be prefixed with a special comment that we are 382 | calling a **Parameter Annotation**. The parameter annotation lets you set the 383 | exact fact path that shall be set to the following parameter. The syntax is: 384 | 385 | `/*@ {path_to_fact} */` 386 | 387 | `path_to_fact` can be either a relative or an absolute path. 388 | 389 | ### Path, Context and Name ### 390 | 391 | #### Path #### 392 | 393 | A path is the full name of a fact or a rule. A path always begin with a "/" 394 | and each segment of the path is separated by "/". Examples: 395 | 396 | - /name 397 | 398 | - /speed/input 399 | 400 | - /speed/inputIsValidInteger 401 | 402 | Are three valid paths. 403 | 404 | #### Name #### 405 | 406 | The name of the rule or the fact is the last string after the last "/" 407 | character. For example, the name in "/speed/input" is "input". 408 | 409 | #### Context #### 410 | 411 | The context is the path less the name. For example, the context in 412 | "/speed/input" is "/speed/". For "/name" the context is simply "/" (aslo 413 | called the root context). 414 | 415 | ### Absolute Fact Reference ### 416 | 417 | Absolute fact reference involves using a fact full name by including the 418 | leading "/". As a convention, a fact can be contextualized using a directory 419 | like notation. Given the above minivan speed example, lets add a message 420 | telling the user that the spped limit of the engine is reached: 421 | 422 | ```javascript 423 | let carModel = { 424 | 425 | name: "Minivan", 426 | message: "", 427 | 428 | speed: { 429 | input: "0", 430 | limit: 140, 431 | value: 0, 432 | 433 | inputIsValidInteger: async function(input) { 434 | let isInt = /^-?\d+$/.test(input); 435 | if (!isInt) { 436 | return { "../message": `Error: '${input}' is not a valid integer.` } 437 | } 438 | return { 439 | "../message": undefined, 440 | value: Number(input) 441 | }; 442 | }, 443 | 444 | valueIsUnderLimit: async function(value, limit) { 445 | if (value > limit) { 446 | return { 447 | value: limit, 448 | "/message": `WARN: The speed input can not exceed the speed limit of ${limit}.` 449 | } 450 | } 451 | } 452 | 453 | // This rule is in the '/speed' context. To reach the /name fact, 454 | // it is possible to use the parameter annotation to set the modelName 455 | // parameter to the /name absolute fact. The same goes for the returned 456 | // /message2 fact. 457 | updateMessage: function(value, limit, /*@ /name */ modelName) { 458 | if (value === limit) { 459 | return {"/message2": `${modelName} ludicrous speed, GO!`}; 460 | } 461 | return {"/message2": undefined}; 462 | } 463 | } 464 | } 465 | ``` 466 | 467 | In the above example, the relative reference "../message" and the absolute 468 | reference "/message" reference the same "message" fact. Since the "message" 469 | fact is directly in the root context and the "input" fact is under the 470 | "/speed/" context, it is easy to see that poping up one directory from 471 | "/speed/" takes the reference to the root context. 472 | 473 | ### Relative Fact Reference ### 474 | 475 | Just like directory reference in a file system, facts can be referenced using 476 | their relative path from the context of the executing rule. This happens when 477 | the path does not begin with "/". When referencing a fact, "../" does pop up 478 | one path element. When the path starts with "/", it references an absolute 479 | path from the root context. To dig down inside the fact tree from the current 480 | context, just write the path elements without a leading "/" 481 | like: "child/of/the/current/context". 482 | 483 | ### Metafacts ### 484 | 485 | There is a special kind of facts that lies within the engine. Meta facts 486 | can be referenced in a rule to know about the context of the current rule 487 | execution. Meta facts cannot trigger rules that reference them. They are 488 | to be injected into rules that have been triggered by any other fact change. 489 | Metafacts are in the "/$/" context. For now there is only two internal 490 | metafacts: 491 | 492 | 1. `/$/maxDepth` contains the value passed to the InfernalEngine constructor 493 | of the same name. It tells how many agenda can be generated in one 494 | inference run before failing. 495 | 496 | 2. `/$/depth` is the current agenda generation. Its value starts at 1 and is 497 | always smaller or equal to `/$/maxDepth`. 498 | 499 | You can add metafacts as well, they will not trigger any rule either. You can 500 | even overwrite the aforementionned metafacts, but that change will last one 501 | inference run or one agenda generation only. Obviously not a good idea but I 502 | don't care since the engine is providing that information to you for your 503 | benefits only. These two metafacts do not affect the actual internal state of 504 | the InfernalEngine. 505 | 506 | ## Infernal Tracing ## 507 | 508 | To trace what happens internally, provide a tracing function with one 509 | parameter to the InfernalEngine constructor's second parameter. The following 510 | code gives an example of that: 511 | 512 | ```javascript 513 | const InfernalEngine = require("infernal-engine"); 514 | const model = require("../models/critterModel"); 515 | 516 | (async () => { 517 | let engine = new InfernalEngine(null, 518 | msg => console.log("-> ", JSON.stringify(msg))); 519 | 520 | console.log("Importing the critterModel:"); 521 | await engine.import(model); 522 | 523 | console.log("Initial model:") 524 | let initialModel = await engine.export(); 525 | console.log(JSON.stringify(initialModel, null, " ")); 526 | 527 | console.log("Importing two facts to be asserted:"); 528 | await engine.import({ 529 | sound: "croaks", 530 | eats: "flies" 531 | }) 532 | 533 | console.log("Inferred model:") 534 | let inferredModel = await engine.export(); 535 | console.log(JSON.stringify(inferredModel, null, " ")); 536 | })(); 537 | ``` 538 | 539 | ### Tracing outputs and resulting fact object ### 540 | 541 | ``` 542 | Importing the critterModel: 543 | -> {"action":"import","object":{"name":"Fritz","sound":"","eats":"","sings":false,"color":"unknown","species":"unknown"}} 544 | -> {"action":"assert","fact":"/name","newValue":"Fritz"} 545 | -> {"action":"assert","fact":"/sound","newValue":""} 546 | -> {"action":"assert","fact":"/eats","newValue":""} 547 | -> {"action":"assert","fact":"/sings","newValue":false} 548 | -> {"action":"assert","fact":"/color","newValue":"unknown"} 549 | -> {"action":"assert","fact":"/species","newValue":"unknown"} 550 | -> {"action":"def","rule":"/isFrog","inputFacts":["/sound","/eats"]} 551 | -> {"action":"addToAgenda","rule":"/isFrog"} 552 | -> {"action":"def","rule":"/isCanary","inputFacts":["/sound","/sings"]} 553 | -> {"action":"addToAgenda","rule":"/isCanary"} 554 | -> {"action":"def","rule":"/isGreen","inputFacts":["/species"]} 555 | -> {"action":"addToAgenda","rule":"/isGreen"} 556 | -> {"action":"def","rule":"/isYellow","inputFacts":["/species"]} 557 | -> {"action":"addToAgenda","rule":"/isYellow"} 558 | -> {"action":"infer","maxGen":50} 559 | -> {"action":"executeAgenda","gen":1,"ruleCount":4} 560 | -> {"action":"execute","rule":"/isFrog","inputs":["",""]} 561 | -> {"action":"execute","rule":"/isCanary","inputs":["",false]} 562 | -> {"action":"execute","rule":"/isGreen","inputs":["unknown"]} 563 | -> {"action":"execute","rule":"/isYellow","inputs":["unknown"]} 564 | Initial facts: 565 | { 566 | "name": "Fritz", 567 | "sound": "", 568 | "eats": "", 569 | "sings": false, 570 | "color": "unknown", 571 | "species": "unknown", 572 | "$": { 573 | "maxGen": 50, 574 | "gen": 1 575 | } 576 | } 577 | Importing two facts to be asserted: 578 | -> {"action":"import","object":{"sound":"croaks","eats":"flies"}} 579 | -> {"action":"assert","fact":"/sound","oldValue":"","newValue":"croaks"} 580 | -> {"action":"addToAgenda","rule":"/isFrog"} 581 | -> {"action":"addToAgenda","rule":"/isCanary"} 582 | -> {"action":"assert","fact":"/eats","oldValue":"","newValue":"flies"} 583 | -> {"action":"addToAgenda","rule":"/isFrog"} 584 | -> {"action":"infer","maxGen":50} 585 | -> {"action":"executeAgenda","gen":1,"ruleCount":2} 586 | -> {"action":"execute","rule":"/isFrog","inputs":["croaks","flies"]} 587 | -> {"action":"assert","fact":"/species","oldValue":"unknown","newValue":"frog"} 588 | -> {"action":"addToAgenda","rule":"/isGreen"} 589 | -> {"action":"addToAgenda","rule":"/isYellow"} 590 | -> {"action":"execute","rule":"/isCanary","inputs":["croaks",false]} 591 | -> {"action":"executeAgenda","gen":2,"ruleCount":2} 592 | -> {"action":"execute","rule":"/isGreen","inputs":["frog"]} 593 | -> {"action":"assert","fact":"/color","oldValue":"unknown","newValue":"green"} 594 | -> {"action":"execute","rule":"/isYellow","inputs":["frog"]} 595 | Inferred facts: 596 | { 597 | "name": "Fritz", 598 | "sound": "croaks", 599 | "eats": "flies", 600 | "sings": false, 601 | "color": "green", 602 | "species": "frog", 603 | "$": { 604 | "maxGen": 50, 605 | "gen": 2 606 | } 607 | } 608 | ``` 609 | 610 | ## Change Notes ## 611 | 612 | ### Version 1.1.2 - 2022/08/01 ### 613 | 614 | - Fix a tracing issue 615 | 616 | ### Version 1.1.1 - 2021/01/09 ### 617 | 618 | - Fix an issue when returning a date or an array from a rule. 619 | 620 | ### Version 1.1.0 - 2021/01/09 ### 621 | 622 | - Deprecate *InfernalEngine#defRule* and replaced it by *InfernalEngine#def* 623 | - Deprecate *InfernalEngine#undefRule* and replaced it by *InfernalEngine#undef* 624 | - Fix some documentation sentences 625 | 626 | ### Version 1.0.1 - 2021/01/04 ### 627 | 628 | - Add the *InfernalEngine#assertAll* method. 629 | - Add the *InfernalEngine#fact* static method to create facts to be consumed by *assertAll*. 630 | - Improve code documentation. 631 | 632 | ### Version 1.0.0 - 2020/12/21 ### 633 | 634 | - Complete rewrite of the engine with ECMAScript 2016. 635 | - Method names have been changed to align with other inference engines (namely by [CLIPS](http://www.clipsrules.net/)) 636 | - Add the possibility to retract facts and undefine rules with wildcards. 637 | - Improve tracing. 638 | -------------------------------------------------------------------------------- /dist/infernal-engine.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.InfernalEngine=e():t.InfernalEngine=e()}(self,(()=>{return t={980:t=>{t.exports=class{constructor(t,e){if("string"!=typeof t)throw new Error("The 'path' parameter must be a string.");this._path=t,this._value=e}get path(){return this._path}get value(){return this._value}}},743:(t,e,a)=>{const s=a(74);t.exports=class{constructor(t,e,a){this.engine=t,this.rule=e,this.path=a,this.facts=[]}async execute(){let t=[];this.facts.forEach((e=>{t.push(this.engine._facts.get(e))})),this.engine._trace&&this.engine._trace({action:"execute",rule:this.path,inputs:t});let e=await this.rule.apply(null,t);if(!e)return;let a=s.getContext(this.path);for(let t in e){if(!e.hasOwnProperty(t))continue;if(!t.startsWith("#")){let i=t.startsWith("/")?t:a+t,r=e[t],n=s.typeof(r);"object"===n?await this.engine.import(r,i):"function"===n?await this.engine.defRule(i,r):await this.engine.assert(i,r);continue}let i=e[t];switch(t){case"#assert":await this.engine.assert(i.path,i.value);break;case"#retract":await this.engine.retract(i.path);break;case"#def":case"#defRule":await this.engine.def(i.path,i.value);break;case"#undef":case"#undefRule":await this.engine.undef(i.path);break;case"#import":await this.engine.import(i.value,i.path);break;default:throw new Error(`Invalid action: '${t}'.`)}}}}},568:(t,e,a)=>{const s=a(74),i=a(743),r=a(980);function n(t){this._trace&&this._trace({action:"undefRule",rule:t}),this._rules.delete(t);for(const[e,a]of this._relations)a.delete(t)}async function h(){this._trace&&this._trace({action:"infer",maxGen:this._maxGen}),this._busy=!0;let t=0;this._facts.set("/$/maxGen",this._maxGen);try{for(;t0;){t++,this._facts.set("/$/gen",t),this._trace&&this._trace({action:"executeAgenda",gen:t,ruleCount:this._agenda.size});let e=this._agenda;this._agenda=new Map;for(const[t,a]of e)await a.execute()}}finally{this._busy=!1}if(t==this._maxGen)throw new Error(`Inference not completed because maximum depth reached (${this._maxGen}). Please review for infinite loop or set the maxDepth property to a larger value.`)}async function c(t,e){let a=e;if(e.endsWith("/")&&(a=e.substring(0,e.length-1)),t instanceof Date||t instanceof Array)return await this.assert(a,t);const s=typeof t;if("function"===s)return await this.defRule(a,t);if("object"!==s)return await this.assert(a,t);for(let e in t)await c.call(this,t[e],`${a}/${e}`)}function o(t){this._relations.has(t)&&this._relations.get(t).forEach((t=>{this._agenda.set(t,this._rules.get(t)),this._trace&&this._trace({action:"addToAgenda",rule:t})}))}function l(t,e,a){let s=e[0];1!==e.length?(void 0===t[s]&&(t[s]={}),l(t[s],e.slice(1),a)):t[s]=a}t.exports=class{constructor(t,e){this._maxGen=t||50,this._trace=e,this._busy=!1,this._facts=new Map,this._rules=new Map,this._relations=new Map,this._agenda=new Map,this._changes=new Set}async peek(t){let e=t.startsWith("/")?t:`/${t}`,a=s.compilePath(e);return this._facts.get(a)}async assert(t,e){let a=t.startsWith("/")?t:`/${t}`,i=s.compilePath(a);var r=this._facts.get(i);if(!(e instanceof Array)&&s.equals(r,e))return;let n="assert";if(void 0!==e)this._facts.set(i,e);else{if(!this._facts.has(i))return void(this._trace&&this._trace({action:"retract",warning:`Cannot retract undefined fact '${i}'.`}));n="retract",this._facts.delete(i),this._relations.delete(i)}this._trace&&this._trace({action:n,fact:i,oldValue:r,newValue:e}),i.startsWith("/$")||(this._changes.add(i),o.call(this,i),this._busy||await h.call(this))}async assertAll(t){if(!(t instanceof Array))throw new Error("The 'facts' parameter must be an Array.");if(0!==t.length){this._trace&&this._trace({action:"assertAll",factCount:t.length}),this._busy=!0;try{for(const e of t){if(!(e instanceof r))throw new Error("The asserted array must contains objects of class Fact only.");await this.assert(e.path,e.value)}}finally{this.busy=!1}await h.call(this)}}async retract(t){if(!t.endsWith("/*"))return void await this.assert(t,void 0);let e=t.startsWith("/")?t:`/${t}`,a=s.compilePath(e),i=a.substr(0,a.length-1);for(const[t,e]of this._facts)t.startsWith(i)&&await this.assert(t,void 0)}async defRule(t,e){await this.def(t,e)}async def(t,e){if("async"!==(e&&e.toString().substring(0,5)))throw new Error("The rule parameter must be an async function.");let a=t.startsWith("/")?t:`/${t}`,r=s.compilePath(a),n=s.getContext(r);if(this._rules.has(r))throw new Error(`Can not define the rule '${r}' because it already exist. Call 'undef' or change the rule path.`);let c=new i(this,e,r),o=s.parseParameters(e);for(const t of o){let e=t.startsWith("/")?t:n+t,a=s.compilePath(e);this._relations.has(a)||this._relations.set(a,new Set),this._relations.get(a).add(r),c.facts.push(a)}this._rules.set(r,c),this._trace&&this._trace({action:"defRule",rule:r,inputFacts:c.facts.slice()}),this._agenda.set(r,c),this._trace&&this._trace({action:"addToAgenda",rule:t}),this._busy||await h.call(this)}async undefRule(t){await this.undef(t)}async undef(t){let e=t.startsWith("/")?t:`/${t}`,a=s.compilePath(e);if(!a.endsWith("/*"))return void n.call(this,a);let i=a.substr(0,a.length-1);for(const[t,e]of this._rules)t.startsWith(i)&&n.call(this,t)}async import(t,e){this._trace&&this._trace({action:"import",object:t});let a=this._busy;this._busy=!0;try{await c.call(this,t,e||""),a||await h.call(this)}finally{a||(this._busy=!1)}}async export(t){let e=t||"/";e.startsWith("/")||(e=`/${e}`);let a={};for(const[t,s]of this._facts)t.startsWith(e)&&l(a,t.substring(e.length).replace(/\//g," ").trim().split(" "),s);return a}async exportChanges(){let t={};for(const e of this._changes)l(t,e.replace(/\//g," ").trim().split(" "),this._facts.get(e));return this._changes.clear(),t}async reset(){this._changes.clear()}static fact(t,e){return new r(t,e)}}},74:t=>{t.exports={equals:function(t,e){var a=t;t instanceof Date&&(a=t.getTime());var s=e;return e instanceof Date&&(s=e.getTime()),a===s},parseParameters:function*(t){let a=(t.toString().split(")")[0]+")").replace(/\s+/g," "),s=/\((.+?)\)|= ?(?:async)? ?(\w+) ?=>/g.exec(a);if(!s)return;let i=s[1],r=e.exec(i);do{yield r[1]||r[2],r=e.exec(i)}while(r)},getContext:function(t){return t.replace(a,"")},compilePath:function(t){let e=t,a=t.replace(s,"");for(;a!=e;)e=a,a=e.replace(s,"");if(e.startsWith("/.."))throw new Error(`Unable to compile the path '${t}' properly.`);return e.replace(i,"")},typeof:function(t){return t instanceof Date?"date":t instanceof Array?"array":typeof t}};const e=/(?:\/\*?@ *([\w/.]+?) *\*\/ *\w+,?)|[(,]? *(\w+) *[,)]?/g,a=/[^/]+?$/,s=/\/[^/.]+\/\.\./,i=/\.\//g}},e={},function a(s){var i=e[s];if(void 0!==i)return i.exports;var r=e[s]={exports:{}};return t[s](r,r.exports,a),r.exports}(568);var t,e})); 2 | //# sourceMappingURL=infernal-engine.js.map -------------------------------------------------------------------------------- /dist/infernal-engine.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"infernal-engine.js","mappings":"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","sources":["webpack://InfernalEngine/webpack/universalModuleDefinition","webpack://InfernalEngine/./lib/Fact.js","webpack://InfernalEngine/./lib/RuleContext.js","webpack://InfernalEngine/./lib/index.js","webpack://InfernalEngine/./lib/infernalUtils.js","webpack://InfernalEngine/webpack/bootstrap","webpack://InfernalEngine/webpack/startup"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"InfernalEngine\"] = factory();\n\telse\n\t\troot[\"InfernalEngine\"] = factory();\n})(self, () => {\nreturn ","\n\n\n/**\n * The Fact class stores the path and the value of a Fact instance.\n */\nclass Fact {\n \n /**\n * \n * @param {string} path The mandatory path of the fact.\n * @param {any} value The optional value of the fact. If letft undefined, the fact will be\n * retracted if it exists.\n */\n constructor(path, value) {\n if (typeof path !== \"string\") {\n throw new Error(\"The 'path' parameter must be a string.\");\n }\n this._path = path;\n this._value = value;\n }\n\n /**\n * Gets the fact path.\n */\n get path() {\n return this._path;\n }\n\n /**\n * Gets the fact value.\n */\n get value() {\n return this._value;\n }\n\n};\n\n\nmodule.exports = Fact;","const infernalUtils = require(\"./infernalUtils\");\n\n/**\n * This class is not exposed. Put a rule into its context to streamline rule execution.\n */\nclass RuleContext {\n\n\n /**\n * Creates an instance of the rule context with the engine, the rule and its path.\n * \n * @param {InfernalEngine} engine The parent InfernalEngine\n * @param {Function} rule The rule to execute.\n * @param {String} path The path of the rule.\n */\n constructor(engine, rule, path) {\n this.engine = engine;\n this.rule = rule;\n this.path = path;\n this.facts = [];\n }\n\n /**\n * Execute the rule within its context returning the \n * resulting object to its contextualized facts.\n */\n async execute() {\n let params = [];\n this.facts.forEach(inputFact => {\n params.push(this.engine._facts.get(inputFact));\n });\n\n if(this.engine._trace) {\n this.engine._trace({\n action: \"execute\",\n rule: this.path,\n inputs: params\n });\n }\n\n let result = await this.rule.apply(null, params);\n if (!result) {\n return;\n }\n\n let context = infernalUtils.getContext(this.path);\n for (let key in result) {\n if (!result.hasOwnProperty(key)) continue;\n\n if (!key.startsWith(\"#\")) {\n let path = key.startsWith(\"/\") ? key : context + key;\n let value = result[key];\n let valueType = infernalUtils.typeof(value);\n if (valueType === \"object\") {\n await this.engine.import(value, path);\n } \n else if (valueType === \"function\") {\n await this.engine.defRule(path, value);\n }\n else {\n await this.engine.assert(path, value);\n }\n continue;\n }\n\n let action = result[key];\n switch (key) {\n\n case \"#assert\":\n await this.engine.assert(action.path, action.value);\n break;\n \n case \"#retract\":\n await this.engine.retract(action.path);\n break;\n\n case \"#def\":\n case \"#defRule\":\n await this.engine.def(action.path, action.value);\n break;\n\n case \"#undef\":\n case \"#undefRule\":\n await this.engine.undef(action.path);\n break;\n\n case \"#import\":\n await this.engine.import(action.value, action.path);\n break;\n\n default:\n throw new Error(`Invalid action: '${key}'.`);\n }\n }\n }\n}\n\nmodule.exports = RuleContext;","\nconst infernalUtils = require(\"./infernalUtils\");\nconst RuleContext = require(\"./RuleContext\");\nconst Fact = require(\"./Fact\");\n\n\n/**\n * This is the inference engine class.\n */\nclass InfernalEngine {\n\n /**\n * Create a new InfernalEngine instance. \n * @param {Number} [maxGen=50] The maximum number of agenda generation\n * when executing inference. \n * @param {Function=} trace A tracing function that will be called with a trace\n * object parameter.\n */\n constructor(maxGen, trace) {\n this._maxGen = maxGen || 50;\n this._trace = trace;\n this._busy = false;\n this._facts = new Map();\n this._rules = new Map();\n this._relations = new Map();\n this._agenda = new Map();\n this._changes = new Set();\n }\n\n /**\n * Returns the fact value for a given path.\n * @param {String} path The full path to the desired fact.\n * @return {Promise<*>} The fact value.\n */\n async peek(path) {\n let factpath = path.startsWith(\"/\") ? path : `/${path}`;\n let compiledpath = infernalUtils.compilePath(factpath);\n return this._facts.get(compiledpath);\n }\n\n /**\n * Asserts a new fact or update an existing fact for the given path with\n * the provided value. Asserting a fact with an undefined value will \n * retract the fact if it exists.\n * @param {String} path The full path to the desired fact.\n * @param {*} value The fact value to set, must be a scalar.\n */\n async assert(path, value) {\n let factpath = path.startsWith(\"/\") ? path : `/${path}`;\n let compiledpath = infernalUtils.compilePath(factpath);\n var oldValue = this._facts.get(compiledpath);\n\n if (!(value instanceof Array) && infernalUtils.equals(oldValue, value)) {\n // GTFO if the value is not an array and the received scalar value does not change\n // the fact value.\n return;\n }\n\n let action = \"assert\";\n if (value !== undefined) {\n this._facts.set(compiledpath, value);\n }\n else {\n if (!this._facts.has(compiledpath)) {\n // the fact does not exist.\n if (this._trace) {\n this._trace({\n action: \"retract\",\n warning: `Cannot retract undefined fact '${compiledpath}'.`\n });\n }\n return;\n }\n action = \"retract\";\n this._facts.delete(compiledpath);\n this._relations.delete(compiledpath);\n }\n\n if (this._trace) {\n this._trace({\n action: action,\n fact: compiledpath,\n oldValue: oldValue,\n newValue: value\n });\n }\n\n // If the path do not reference a meta-fact\n if (!compiledpath.startsWith(\"/$\")) {\n this._changes.add(compiledpath);\n _addToAgenda.call(this, compiledpath);\n if (!this._busy) {\n await _infer.call(this);\n }\n }\n }\n\n /**\n * Asserts all recieved facts.\n * @param {Array} facts A list of facts to assert at in one go.\n */\n async assertAll(facts) {\n if (!(facts instanceof Array)) {\n throw new Error(\"The 'facts' parameter must be an Array.\");\n }\n if (facts.length === 0) {\n return;\n }\n if (this._trace) {\n this._trace({\n action: \"assertAll\",\n factCount: facts.length\n });\n }\n this._busy = true;\n try {\n for (const fact of facts) {\n if (!(fact instanceof Fact)) {\n throw new Error(\"The asserted array must contains objects of class Fact only.\");\n }\n await this.assert(fact.path, fact.value);\n }\n }\n finally {\n this.busy = false;\n }\n await _infer.call(this);\n }\n\n /**\n * Retracts a fact or multiple facts recursively if the path ends with '/*'.\n * @param {String} path The path to the fact to retract.\n */\n async retract(path) {\n if (!path.endsWith(\"/*\")) {\n await this.assert(path, undefined);\n return;\n }\n\n let factpath = path.startsWith(\"/\") ? path : `/${path}`;\n let compiledPath = infernalUtils.compilePath(factpath);\n let pathPrefix = compiledPath.substr(0, compiledPath.length - 1);\n for (const [factPath, _] of this._facts) {\n if (!factPath.startsWith(pathPrefix)) continue;\n await this.assert(factPath, undefined);\n }\n }\n\n /**\n * @deprecated Use {@link InfernalEngine#def} instead.\n * Add a rule to the engine's ruleset and launche the inference.\n * @param {String} path The path where to save the rule at.\n * @param {Function} rule The rule to add. Must be async.\n */\n async defRule(path, rule) {\n await this.def(path, rule);\n }\n\n /**\n * Add a rule to the engine's ruleset and launche the inference.\n * @param {String} path The path where to save the rule at.\n * @param {Function} rule The rule to add. Must be async.\n */\n async def(path, rule) {\n let prefix = rule && rule.toString().substring(0,5);\n if (prefix !== \"async\") {\n throw new Error(\"The rule parameter must be an async function.\");\n }\n \n let rulepath = path.startsWith(\"/\") ? path : `/${path}`;\n let compiledRulepath = infernalUtils.compilePath(rulepath);\n let context = infernalUtils.getContext(compiledRulepath);\n\n if (this._rules.has(compiledRulepath)) {\n throw new Error(`Can not define the rule '${compiledRulepath}' because it ` +\n \"already exist. Call 'undef' or change the rule path.\");\n }\n\n let ruleContext = new RuleContext(this, rule, compiledRulepath)\n let parameters = infernalUtils.parseParameters(rule);\n for (const param of parameters) {\n let factpath = param.startsWith(\"/\") ? param : context + param;\n let compiledFactpath = infernalUtils.compilePath(factpath);\n if (!this._relations.has(compiledFactpath))\n this._relations.set(compiledFactpath, new Set());\n this._relations.get(compiledFactpath).add(compiledRulepath);\n ruleContext.facts.push(compiledFactpath);\n }\n \n this._rules.set(compiledRulepath, ruleContext);\n if (this._trace) {\n this._trace({\n action: \"defRule\", \n rule: compiledRulepath,\n inputFacts: ruleContext.facts.slice()\n });\n }\n\n this._agenda.set(compiledRulepath, ruleContext);\n if (this._trace) {\n this._trace({\n action: \"addToAgenda\",\n rule: path\n });\n }\n\n if (!this._busy) {\n await _infer.call(this);\n }\n }\n\n\n /**\n * @deprecated Use {@link InfernalEngine#undef} instead.\n * Undefine a rule at the given path or a group of rules if the path ends with '/*'.\n * @param {String} path The path to the rule to be undefined.\n */\n async undefRule(path) {\n await this.undef(path);\n }\n\n /**\n * Undefine a rule at the given path or a group of rules if the path ends with '/*'.\n * @param {String} path The path to the rule to be undefined.\n */\n async undef(path) {\n let rulepath = path.startsWith(\"/\") ? path : `/${path}`;\n let compiledRulepath = infernalUtils.compilePath(rulepath);\n\n if (!compiledRulepath.endsWith(\"/*\")) {\n _deleteRule.call(this, compiledRulepath);\n return;\n }\n\n let pathPrefix = compiledRulepath.substr(0, compiledRulepath.length - 1);\n for (const [path, _] of this._rules) {\n if (!path.startsWith(pathPrefix)) continue;\n _deleteRule.call(this, path);\n }\n }\n\n /**\n * Import the given Javascript object into the engine. Scalar values and arrays as facts,\n * functions as rules. Launches the inference on any new rules and any existing rules\n * triggered by importing the object facts. Infers only when eveything have been imported.\n * @param {Object} obj The object to import.\n * @param {String} context The path where the object will be imported.\n */\n async import(obj, context) {\n if (this._trace) {\n this._trace({\n action: \"import\", \n object: obj\n });\n }\n let superBusy = this._busy; // true when called while infering.\n this._busy = true;\n try {\n await _import.call(this, obj, context || \"\");\n if (!superBusy) {\n // not already infering, start the inference.\n await _infer.call(this);\n }\n }\n finally {\n if (!superBusy) {\n // Not already infering, reset the busy state.\n this._busy = false;\n }\n }\n }\n\n /**\n * Export internal facts from the given optional path as a JSON object. Do not export rules.\n * @param {String} [context=\"/\"] The context to export as an object.\n * @return {object} a JSON object representation of the engine internal state.\n */\n async export(context) {\n let targetContext = context || \"/\";\n if (!targetContext.startsWith(\"/\")) {\n targetContext = `/${targetContext}`;\n }\n let obj = {};\n for (const [key, value] of this._facts) {\n if (key.startsWith(targetContext)) {\n let subkeys = key\n .substring(targetContext.length)\n .replace(/\\//g, \" \")\n .trim()\n .split(\" \");\n _deepSet(obj, subkeys, value);\n }\n }\n return obj;\n }\n\n\n /**\n * Exports all changed facts since the last call to exportChanges or\n * [reset]{@link InfernalEngine#reset} as a Javascript object. Reset the change tracker.\n * @return a JSON object containing the cumulative changes since last call.\n */\n async exportChanges() {\n let obj = {};\n for (const key of this._changes) {\n let subkeys = key\n .replace(/\\//g, \" \")\n .trim()\n .split(\" \");\n _deepSet(obj, subkeys, this._facts.get(key));\n }\n this._changes.clear();\n return obj;\n }\n\n /**\n * Resets the change tracker.\n */\n async reset() {\n this._changes.clear();\n }\n\n /**\n * Create a new Fact with the given path and value.\n * @param {string} path Mandatory path of the fact.\n * @param {any} value Value of the fact, can be 'undefined' to retract the given fact.\n */\n static fact(path, value) {\n return new Fact(path, value);\n }\n\n}\n\n\n\n// Private\n\n\nfunction _deleteRule(path) {\n if (this._trace) {\n this._trace({\n action: \"undefRule\", \n rule: path\n });\n }\n this._rules.delete(path);\n\n // TODO: Target relations using the rule's parameter instead of looping blindly.\n for (const [_, rules] of this._relations) {\n rules.delete(path);\n }\n}\n\n // Execute inference and return a promise. At the begining of the inference, sets the \n // '/$/maxGen' fact to make it available to rules that would be interested in this value.\n // Upon each loop, the engine sets the '/$/gen' value indicating the agenda generation the\n // inference is currently at.\nasync function _infer() {\n if (this._trace) {\n this._trace({\n action: \"infer\",\n maxGen: this._maxGen\n });\n }\n\n this._busy = true;\n let gen = 0;\n this._facts.set(\"/$/maxGen\", this._maxGen); //metafacts do not trigger rules\n\n try {\n while (gen < this._maxGen && this._agenda.size > 0) {\n gen++;\n this._facts.set(\"/$/gen\", gen); // metafacts do not trigger rules\n if (this._trace) {\n this._trace({\n action: \"executeAgenda\",\n gen: gen,\n ruleCount: this._agenda.size\n });\n }\n let currentAgenda = this._agenda;\n this._agenda = new Map();\n for (const [_, rulectx] of currentAgenda) {\n await rulectx.execute();\n }\n }\n }\n finally {\n this._busy = false;\n }\n\n if (gen == this._maxGen) {\n throw new Error(\"Inference not completed because maximum depth \" +\n `reached (${this._maxGen}). Please review for infinite loop or set the ` +\n \"maxDepth property to a larger value.\");\n }\n}\n\n\nasync function _import(obj, context) {\n let targetContext = context;\n if (context.endsWith(\"/\")) {\n targetContext = context.substring(0, context.length-1);\n }\n\n // Set an object that needs to be handled like scalar value.\n if (obj instanceof Date || obj instanceof Array) {\n return await this.assert(targetContext, obj);\n }\n\n const objtype = typeof obj;\n\n // Handle rules as they come by.\n if (objtype === \"function\") {\n return await this.defRule(targetContext, obj);\n }\n\n // Set scalar value\n if (objtype !== \"object\") {\n return await this.assert(targetContext, obj);\n }\n\n // Drill down into the object to add other facts and rules.\n for (let member in obj) {\n await _import.call(this,\n obj[member],\n `${targetContext}/${member}`);\n }\n}\n\nfunction _addToAgenda(factName) {\n if (this._relations.has(factName)) {\n let rules = this._relations.get(factName);\n rules.forEach(ruleName => {\n this._agenda.set(ruleName, this._rules.get(ruleName));\n if (this._trace) {\n this._trace({\n action: \"addToAgenda\",\n rule: ruleName\n });\n }\n });\n }\n}\n\nfunction _deepSet(target, keys, value) {\n let key = keys[0];\n if (keys.length === 1) {\n target[key] = value;\n return;\n }\n if (typeof target[key] === \"undefined\") {\n target[key] = {};\n }\n _deepSet(target[key], keys.slice(1), value)\n}\n\n\nmodule.exports = InfernalEngine;","\nmodule.exports = {\n equals: equals,\n parseParameters: parseFactPaths,\n getContext: getContext,\n compilePath: compilePath,\n typeof: _typeof\n};\n\n//https://regex101.com/r/zYhguP/6/\nconst paramRegex = /(?:\\/\\*?@ *([\\w/.]+?) *\\*\\/ *\\w+,?)|[(,]? *(\\w+) *[,)]?/g;\nconst trailingTermRemovalRegex = /[^/]+?$/;\nconst pathCompactionRegex = /\\/[^/.]+\\/\\.\\./;\nconst currentPathRemoval = /\\.\\//g;\n\n/**\n * Compare two values and return true if they are equal. Fix date comparison issue and insure\n * both types are the same.\n * \n * @param {*} a The first value used to compare.\n * @param {*} b The second value used to caompare.\n */\nfunction equals(a, b) {\n var aValue = a;\n if (a instanceof Date) {\n aValue = a.getTime(); \n }\n var bValue = b;\n if (b instanceof Date) {\n bValue = b.getTime();\n }\n return (aValue === bValue);\n}\n\n\nfunction _typeof(o) {\n if (o instanceof Date) {\n return \"date\";\n }\n else if (o instanceof Array) {\n return \"array\";\n }\n else {\n return typeof o;\n }\n}\n\n/**\n * Generator that parses all fact paths derived from the given rule parameters. Either return the\n * parameter name or the path contained by the optional attribute comment. Single parameter lambda\n * function must put the parameter between parenthesis to be parsed properly.\n * \n * @example\n * \n * Using an attribute comment:\n * \n * async function(/¤@ /path/to/fact ¤/ param1, param2) {}\n * \n * In the above example: \n * \n * 1) ¤ replaces * because using * and / one after the other ends the documentation comment.\n * 2) returned fact paths would be ['/path/to/fact', 'param2']\n * \n * @param {AsyncFunction} rule The rule \n */\nfunction* parseFactPaths(rule) {\n let allParamsRegex = /\\((.+?)\\)|= ?(?:async)? ?(\\w+) ?=>/g;\n let paramCode = (rule.toString().split(')')[0] + ')');\n let paramCodeWithoutEol = paramCode.replace(/\\s+/g, \" \");\n let allParamMatch = allParamsRegex.exec(paramCodeWithoutEol);\n if (!allParamMatch) return;\n\n let allParams = allParamMatch[1];\n let paramMatch = paramRegex.exec(allParams);\n do {\n yield (paramMatch[1] || paramMatch[2]);\n paramMatch = paramRegex.exec(allParams);\n } \n while (paramMatch)\n}\n\n\n/**\n * Extracts the context from the fact or rule name. Must be a compiled path, not a path that\n * contains \"../\" or \"./\".\n * \n * @example\n * \n * Given the rule path: '/path/to/some/rule'\n * The returned value would be: '/path/to/some'\n * \n * @param {String} compiledPath The compiled path to extract the context from.\n */\nfunction getContext(compiledPath) {\n return compiledPath.replace(trailingTermRemovalRegex, \"\");\n}\n\n/**\n * Create a compacted fact name from a given fact name. Path do not end with a trailing '/'.\n * \n * '../' Denote the parent term. Move up one term in the context or path.\n * './' Denote the current term. Removed from the context or path.\n * \n * Examples: \n * \n * '/a/given/../correct/./path' is compiled to '/a/correct/path'\n * \n * @param {String} path The path or context to compile into a context.\n */\nfunction compilePath(path) {\n let current = path;\n let next = path.replace(pathCompactionRegex, \"\");\n while (next != current) {\n current = next;\n next = current.replace(pathCompactionRegex, \"\");\n }\n if (current.startsWith(\"/..\")) {\n throw new Error(`Unable to compile the path '${path}' properly.`);\n }\n return current.replace(currentPathRemoval, \"\");\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nvar __webpack_exports__ = __webpack_require__(568);\n"],"names":["root","factory","exports","module","define","amd","self","constructor","path","value","Error","this","_path","_value","infernalUtils","engine","rule","facts","async","params","forEach","inputFact","push","_facts","get","_trace","action","inputs","result","apply","context","getContext","key","hasOwnProperty","startsWith","valueType","typeof","import","defRule","assert","retract","def","undef","RuleContext","Fact","_deleteRule","_rules","delete","_","rules","_relations","_infer","maxGen","_maxGen","_busy","gen","set","_agenda","size","ruleCount","currentAgenda","Map","rulectx","execute","_import","obj","targetContext","endsWith","substring","length","Date","Array","objtype","member","call","_addToAgenda","factName","has","ruleName","_deepSet","target","keys","slice","trace","_changes","Set","factpath","compiledpath","compilePath","oldValue","equals","undefined","warning","fact","newValue","add","factCount","busy","compiledPath","pathPrefix","substr","factPath","toString","rulepath","compiledRulepath","ruleContext","parameters","parseParameters","param","compiledFactpath","inputFacts","object","superBusy","replace","trim","split","clear","static","a","b","aValue","getTime","bValue","paramCodeWithoutEol","allParamMatch","exec","allParams","paramMatch","paramRegex","trailingTermRemovalRegex","current","next","pathCompactionRegex","currentPathRemoval","o","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/KnowledgeIFPschool.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formix/infernal-engine/c101d72d66be12bec1adf2348400139013ef2a4e/docs/KnowledgeIFPschool.pdf -------------------------------------------------------------------------------- /jsdoc/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "encoding": "utf8" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/Fact.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /** 5 | * The Fact class stores the path and the value of a Fact instance. 6 | */ 7 | class Fact { 8 | 9 | /** 10 | * 11 | * @param {string} path The mandatory path of the fact. 12 | * @param {any} value The optional value of the fact. If letft undefined, the fact will be 13 | * retracted if it exists. 14 | */ 15 | constructor(path, value) { 16 | if (typeof path !== "string") { 17 | throw new Error("The 'path' parameter must be a string."); 18 | } 19 | this._path = path; 20 | this._value = value; 21 | } 22 | 23 | /** 24 | * Gets the fact path. 25 | */ 26 | get path() { 27 | return this._path; 28 | } 29 | 30 | /** 31 | * Gets the fact value. 32 | */ 33 | get value() { 34 | return this._value; 35 | } 36 | 37 | }; 38 | 39 | 40 | module.exports = Fact; -------------------------------------------------------------------------------- /lib/RuleContext.js: -------------------------------------------------------------------------------- 1 | const infernalUtils = require("./infernalUtils"); 2 | 3 | /** 4 | * This class is not exposed. Put a rule into its context to streamline rule execution. 5 | */ 6 | class RuleContext { 7 | 8 | 9 | /** 10 | * Creates an instance of the rule context with the engine, the rule and its path. 11 | * 12 | * @param {InfernalEngine} engine The parent InfernalEngine 13 | * @param {Function} rule The rule to execute. 14 | * @param {String} path The path of the rule. 15 | */ 16 | constructor(engine, rule, path) { 17 | this.engine = engine; 18 | this.rule = rule; 19 | this.path = path; 20 | this.facts = []; 21 | } 22 | 23 | /** 24 | * Execute the rule within its context returning the 25 | * resulting object to its contextualized facts. 26 | */ 27 | async execute() { 28 | let params = []; 29 | this.facts.forEach(inputFact => { 30 | params.push(this.engine._facts.get(inputFact)); 31 | }); 32 | 33 | if(this.engine._trace) { 34 | this.engine._trace({ 35 | action: "execute", 36 | rule: this.path, 37 | inputs: params 38 | }); 39 | } 40 | 41 | let result = await this.rule.apply(null, params); 42 | if (!result) { 43 | return; 44 | } 45 | 46 | let context = infernalUtils.getContext(this.path); 47 | for (let key in result) { 48 | if (!result.hasOwnProperty(key)) continue; 49 | 50 | if (!key.startsWith("#")) { 51 | let path = key.startsWith("/") ? key : context + key; 52 | let value = result[key]; 53 | let valueType = infernalUtils.typeof(value); 54 | if (valueType === "object") { 55 | await this.engine.import(value, path); 56 | } 57 | else if (valueType === "function") { 58 | await this.engine.defRule(path, value); 59 | } 60 | else { 61 | await this.engine.assert(path, value); 62 | } 63 | continue; 64 | } 65 | 66 | let action = result[key]; 67 | switch (key) { 68 | 69 | case "#assert": 70 | await this.engine.assert(action.path, action.value); 71 | break; 72 | 73 | case "#retract": 74 | await this.engine.retract(action.path); 75 | break; 76 | 77 | case "#def": 78 | case "#defRule": 79 | await this.engine.def(action.path, action.value); 80 | break; 81 | 82 | case "#undef": 83 | case "#undefRule": 84 | await this.engine.undef(action.path); 85 | break; 86 | 87 | case "#import": 88 | await this.engine.import(action.value, action.path); 89 | break; 90 | 91 | default: 92 | throw new Error(`Invalid action: '${key}'.`); 93 | } 94 | } 95 | } 96 | } 97 | 98 | module.exports = RuleContext; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | const infernalUtils = require("./infernalUtils"); 3 | const RuleContext = require("./RuleContext"); 4 | const Fact = require("./Fact"); 5 | 6 | 7 | /** 8 | * This is the inference engine class. 9 | */ 10 | class InfernalEngine { 11 | 12 | /** 13 | * Create a new InfernalEngine instance. 14 | * @param {Number} [maxGen=50] The maximum number of agenda generation 15 | * when executing inference. 16 | * @param {Function=} trace A tracing function that will be called with a trace 17 | * object parameter. 18 | */ 19 | constructor(maxGen, trace) { 20 | this._maxGen = maxGen || 50; 21 | this._trace = trace; 22 | this._busy = false; 23 | this._facts = new Map(); 24 | this._rules = new Map(); 25 | this._relations = new Map(); 26 | this._agenda = new Map(); 27 | this._changes = new Set(); 28 | } 29 | 30 | /** 31 | * Returns the fact value for a given path. 32 | * @param {String} path The full path to the desired fact. 33 | * @return {Promise<*>} The fact value. 34 | */ 35 | async peek(path) { 36 | let factpath = path.startsWith("/") ? path : `/${path}`; 37 | let compiledpath = infernalUtils.compilePath(factpath); 38 | return this._facts.get(compiledpath); 39 | } 40 | 41 | /** 42 | * Asserts a new fact or update an existing fact for the given path with 43 | * the provided value. Asserting a fact with an undefined value will 44 | * retract the fact if it exists. 45 | * @param {String} path The full path to the desired fact. 46 | * @param {*} value The fact value to set, must be a scalar. 47 | */ 48 | async assert(path, value) { 49 | let factpath = path.startsWith("/") ? path : `/${path}`; 50 | let compiledpath = infernalUtils.compilePath(factpath); 51 | var oldValue = this._facts.get(compiledpath); 52 | 53 | if (!(value instanceof Array) && infernalUtils.equals(oldValue, value)) { 54 | // GTFO if the value is not an array and the received scalar value does not change 55 | // the fact value. 56 | return; 57 | } 58 | 59 | let action = "assert"; 60 | if (value !== undefined) { 61 | this._facts.set(compiledpath, value); 62 | } 63 | else { 64 | if (!this._facts.has(compiledpath)) { 65 | // the fact does not exist. 66 | if (this._trace) { 67 | this._trace({ 68 | action: "retract", 69 | warning: `Cannot retract undefined fact '${compiledpath}'.` 70 | }); 71 | } 72 | return; 73 | } 74 | action = "retract"; 75 | this._facts.delete(compiledpath); 76 | this._relations.delete(compiledpath); 77 | } 78 | 79 | if (this._trace) { 80 | this._trace({ 81 | action: action, 82 | fact: compiledpath, 83 | oldValue: oldValue, 84 | newValue: value 85 | }); 86 | } 87 | 88 | // If the path do not reference a meta-fact 89 | if (!compiledpath.startsWith("/$")) { 90 | this._changes.add(compiledpath); 91 | _addToAgenda.call(this, compiledpath); 92 | if (!this._busy) { 93 | await _infer.call(this); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Asserts all recieved facts. 100 | * @param {Array} facts A list of facts to assert at in one go. 101 | */ 102 | async assertAll(facts) { 103 | if (!(facts instanceof Array)) { 104 | throw new Error("The 'facts' parameter must be an Array."); 105 | } 106 | if (facts.length === 0) { 107 | return; 108 | } 109 | if (this._trace) { 110 | this._trace({ 111 | action: "assertAll", 112 | factCount: facts.length 113 | }); 114 | } 115 | this._busy = true; 116 | try { 117 | for (const fact of facts) { 118 | if (!(fact instanceof Fact)) { 119 | throw new Error("The asserted array must contains objects of class Fact only."); 120 | } 121 | await this.assert(fact.path, fact.value); 122 | } 123 | } 124 | finally { 125 | this.busy = false; 126 | } 127 | await _infer.call(this); 128 | } 129 | 130 | /** 131 | * Retracts a fact or multiple facts recursively if the path ends with '/*'. 132 | * @param {String} path The path to the fact to retract. 133 | */ 134 | async retract(path) { 135 | if (!path.endsWith("/*")) { 136 | await this.assert(path, undefined); 137 | return; 138 | } 139 | 140 | let factpath = path.startsWith("/") ? path : `/${path}`; 141 | let compiledPath = infernalUtils.compilePath(factpath); 142 | let pathPrefix = compiledPath.substr(0, compiledPath.length - 1); 143 | for (const [factPath, _] of this._facts) { 144 | if (!factPath.startsWith(pathPrefix)) continue; 145 | await this.assert(factPath, undefined); 146 | } 147 | } 148 | 149 | /** 150 | * @deprecated Use {@link InfernalEngine#def} instead. 151 | * Add a rule to the engine's ruleset and launche the inference. 152 | * @param {String} path The path where to save the rule at. 153 | * @param {Function} rule The rule to add. Must be async. 154 | */ 155 | async defRule(path, rule) { 156 | await this.def(path, rule); 157 | } 158 | 159 | /** 160 | * Add a rule to the engine's ruleset and launche the inference. 161 | * @param {String} path The path where to save the rule at. 162 | * @param {Function} rule The rule to add. Must be async. 163 | */ 164 | async def(path, rule) { 165 | let prefix = rule && rule.toString().substring(0,5); 166 | if (prefix !== "async") { 167 | throw new Error("The rule parameter must be an async function."); 168 | } 169 | 170 | let rulepath = path.startsWith("/") ? path : `/${path}`; 171 | let compiledRulepath = infernalUtils.compilePath(rulepath); 172 | let context = infernalUtils.getContext(compiledRulepath); 173 | 174 | if (this._rules.has(compiledRulepath)) { 175 | throw new Error(`Can not define the rule '${compiledRulepath}' because it ` + 176 | "already exist. Call 'undef' or change the rule path."); 177 | } 178 | 179 | let ruleContext = new RuleContext(this, rule, compiledRulepath) 180 | let parameters = infernalUtils.parseParameters(rule); 181 | for (const param of parameters) { 182 | let factpath = param.startsWith("/") ? param : context + param; 183 | let compiledFactpath = infernalUtils.compilePath(factpath); 184 | if (!this._relations.has(compiledFactpath)) 185 | this._relations.set(compiledFactpath, new Set()); 186 | this._relations.get(compiledFactpath).add(compiledRulepath); 187 | ruleContext.facts.push(compiledFactpath); 188 | } 189 | 190 | this._rules.set(compiledRulepath, ruleContext); 191 | if (this._trace) { 192 | this._trace({ 193 | action: "defRule", 194 | rule: compiledRulepath, 195 | inputFacts: ruleContext.facts.slice() 196 | }); 197 | } 198 | 199 | this._agenda.set(compiledRulepath, ruleContext); 200 | if (this._trace) { 201 | this._trace({ 202 | action: "addToAgenda", 203 | rule: path 204 | }); 205 | } 206 | 207 | if (!this._busy) { 208 | await _infer.call(this); 209 | } 210 | } 211 | 212 | 213 | /** 214 | * @deprecated Use {@link InfernalEngine#undef} instead. 215 | * Undefine a rule at the given path or a group of rules if the path ends with '/*'. 216 | * @param {String} path The path to the rule to be undefined. 217 | */ 218 | async undefRule(path) { 219 | await this.undef(path); 220 | } 221 | 222 | /** 223 | * Undefine a rule at the given path or a group of rules if the path ends with '/*'. 224 | * @param {String} path The path to the rule to be undefined. 225 | */ 226 | async undef(path) { 227 | let rulepath = path.startsWith("/") ? path : `/${path}`; 228 | let compiledRulepath = infernalUtils.compilePath(rulepath); 229 | 230 | if (!compiledRulepath.endsWith("/*")) { 231 | _deleteRule.call(this, compiledRulepath); 232 | return; 233 | } 234 | 235 | let pathPrefix = compiledRulepath.substr(0, compiledRulepath.length - 1); 236 | for (const [path, _] of this._rules) { 237 | if (!path.startsWith(pathPrefix)) continue; 238 | _deleteRule.call(this, path); 239 | } 240 | } 241 | 242 | /** 243 | * Import the given Javascript object into the engine. Scalar values and arrays as facts, 244 | * functions as rules. Launches the inference on any new rules and any existing rules 245 | * triggered by importing the object facts. Infers only when eveything have been imported. 246 | * @param {Object} obj The object to import. 247 | * @param {String} context The path where the object will be imported. 248 | */ 249 | async import(obj, context) { 250 | if (this._trace) { 251 | this._trace({ 252 | action: "import", 253 | object: obj 254 | }); 255 | } 256 | let superBusy = this._busy; // true when called while infering. 257 | this._busy = true; 258 | try { 259 | await _import.call(this, obj, context || ""); 260 | if (!superBusy) { 261 | // not already infering, start the inference. 262 | await _infer.call(this); 263 | } 264 | } 265 | finally { 266 | if (!superBusy) { 267 | // Not already infering, reset the busy state. 268 | this._busy = false; 269 | } 270 | } 271 | } 272 | 273 | /** 274 | * Export internal facts from the given optional path as a JSON object. Do not export rules. 275 | * @param {String} [context="/"] The context to export as an object. 276 | * @return {object} a JSON object representation of the engine internal state. 277 | */ 278 | async export(context) { 279 | let targetContext = context || "/"; 280 | if (!targetContext.startsWith("/")) { 281 | targetContext = `/${targetContext}`; 282 | } 283 | let obj = {}; 284 | for (const [key, value] of this._facts) { 285 | if (key.startsWith(targetContext)) { 286 | let subkeys = key 287 | .substring(targetContext.length) 288 | .replace(/\//g, " ") 289 | .trim() 290 | .split(" "); 291 | _deepSet(obj, subkeys, value); 292 | } 293 | } 294 | return obj; 295 | } 296 | 297 | 298 | /** 299 | * Exports all changed facts since the last call to exportChanges or 300 | * [reset]{@link InfernalEngine#reset} as a Javascript object. Reset the change tracker. 301 | * @return a JSON object containing the cumulative changes since last call. 302 | */ 303 | async exportChanges() { 304 | let obj = {}; 305 | for (const key of this._changes) { 306 | let subkeys = key 307 | .replace(/\//g, " ") 308 | .trim() 309 | .split(" "); 310 | _deepSet(obj, subkeys, this._facts.get(key)); 311 | } 312 | this._changes.clear(); 313 | return obj; 314 | } 315 | 316 | /** 317 | * Resets the change tracker. 318 | */ 319 | async reset() { 320 | this._changes.clear(); 321 | } 322 | 323 | /** 324 | * Create a new Fact with the given path and value. 325 | * @param {string} path Mandatory path of the fact. 326 | * @param {any} value Value of the fact, can be 'undefined' to retract the given fact. 327 | */ 328 | static fact(path, value) { 329 | return new Fact(path, value); 330 | } 331 | 332 | } 333 | 334 | 335 | 336 | // Private 337 | 338 | 339 | function _deleteRule(path) { 340 | if (this._trace) { 341 | this._trace({ 342 | action: "undefRule", 343 | rule: path 344 | }); 345 | } 346 | this._rules.delete(path); 347 | 348 | // TODO: Target relations using the rule's parameter instead of looping blindly. 349 | for (const [_, rules] of this._relations) { 350 | rules.delete(path); 351 | } 352 | } 353 | 354 | // Execute inference and return a promise. At the begining of the inference, sets the 355 | // '/$/maxGen' fact to make it available to rules that would be interested in this value. 356 | // Upon each loop, the engine sets the '/$/gen' value indicating the agenda generation the 357 | // inference is currently at. 358 | async function _infer() { 359 | if (this._trace) { 360 | this._trace({ 361 | action: "infer", 362 | maxGen: this._maxGen 363 | }); 364 | } 365 | 366 | this._busy = true; 367 | let gen = 0; 368 | this._facts.set("/$/maxGen", this._maxGen); //metafacts do not trigger rules 369 | 370 | try { 371 | while (gen < this._maxGen && this._agenda.size > 0) { 372 | gen++; 373 | this._facts.set("/$/gen", gen); // metafacts do not trigger rules 374 | if (this._trace) { 375 | this._trace({ 376 | action: "executeAgenda", 377 | gen: gen, 378 | ruleCount: this._agenda.size 379 | }); 380 | } 381 | let currentAgenda = this._agenda; 382 | this._agenda = new Map(); 383 | for (const [_, rulectx] of currentAgenda) { 384 | await rulectx.execute(); 385 | } 386 | } 387 | } 388 | finally { 389 | this._busy = false; 390 | } 391 | 392 | if (gen == this._maxGen) { 393 | throw new Error("Inference not completed because maximum depth " + 394 | `reached (${this._maxGen}). Please review for infinite loop or set the ` + 395 | "maxDepth property to a larger value."); 396 | } 397 | } 398 | 399 | 400 | async function _import(obj, context) { 401 | let targetContext = context; 402 | if (context.endsWith("/")) { 403 | targetContext = context.substring(0, context.length-1); 404 | } 405 | 406 | // Set an object that needs to be handled like scalar value. 407 | if (obj instanceof Date || obj instanceof Array) { 408 | return await this.assert(targetContext, obj); 409 | } 410 | 411 | const objtype = typeof obj; 412 | 413 | // Handle rules as they come by. 414 | if (objtype === "function") { 415 | return await this.defRule(targetContext, obj); 416 | } 417 | 418 | // Set scalar value 419 | if (objtype !== "object") { 420 | return await this.assert(targetContext, obj); 421 | } 422 | 423 | // Drill down into the object to add other facts and rules. 424 | for (let member in obj) { 425 | await _import.call(this, 426 | obj[member], 427 | `${targetContext}/${member}`); 428 | } 429 | } 430 | 431 | function _addToAgenda(factName) { 432 | if (this._relations.has(factName)) { 433 | let rules = this._relations.get(factName); 434 | rules.forEach(ruleName => { 435 | this._agenda.set(ruleName, this._rules.get(ruleName)); 436 | if (this._trace) { 437 | this._trace({ 438 | action: "addToAgenda", 439 | rule: ruleName 440 | }); 441 | } 442 | }); 443 | } 444 | } 445 | 446 | function _deepSet(target, keys, value) { 447 | let key = keys[0]; 448 | if (keys.length === 1) { 449 | target[key] = value; 450 | return; 451 | } 452 | if (typeof target[key] === "undefined") { 453 | target[key] = {}; 454 | } 455 | _deepSet(target[key], keys.slice(1), value) 456 | } 457 | 458 | 459 | module.exports = InfernalEngine; -------------------------------------------------------------------------------- /lib/infernalUtils.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | equals: equals, 4 | parseParameters: parseFactPaths, 5 | getContext: getContext, 6 | compilePath: compilePath, 7 | typeof: _typeof 8 | }; 9 | 10 | //https://regex101.com/r/zYhguP/6/ 11 | const paramRegex = /(?:\/\*?@ *([\w/.]+?) *\*\/ *\w+,?)|[(,]? *(\w+) *[,)]?/g; 12 | const trailingTermRemovalRegex = /[^/]+?$/; 13 | const pathCompactionRegex = /\/[^/.]+\/\.\./; 14 | const currentPathRemoval = /\.\//g; 15 | 16 | /** 17 | * Compare two values and return true if they are equal. Fix date comparison issue and insure 18 | * both types are the same. 19 | * 20 | * @param {*} a The first value used to compare. 21 | * @param {*} b The second value used to caompare. 22 | */ 23 | function equals(a, b) { 24 | var aValue = a; 25 | if (a instanceof Date) { 26 | aValue = a.getTime(); 27 | } 28 | var bValue = b; 29 | if (b instanceof Date) { 30 | bValue = b.getTime(); 31 | } 32 | return (aValue === bValue); 33 | } 34 | 35 | 36 | function _typeof(o) { 37 | if (o instanceof Date) { 38 | return "date"; 39 | } 40 | else if (o instanceof Array) { 41 | return "array"; 42 | } 43 | else { 44 | return typeof o; 45 | } 46 | } 47 | 48 | /** 49 | * Generator that parses all fact paths derived from the given rule parameters. Either return the 50 | * parameter name or the path contained by the optional attribute comment. Single parameter lambda 51 | * function must put the parameter between parenthesis to be parsed properly. 52 | * 53 | * @example 54 | * 55 | * Using an attribute comment: 56 | * 57 | * async function(/¤@ /path/to/fact ¤/ param1, param2) {} 58 | * 59 | * In the above example: 60 | * 61 | * 1) ¤ replaces * because using * and / one after the other ends the documentation comment. 62 | * 2) returned fact paths would be ['/path/to/fact', 'param2'] 63 | * 64 | * @param {AsyncFunction} rule The rule 65 | */ 66 | function* parseFactPaths(rule) { 67 | let allParamsRegex = /\((.+?)\)|= ?(?:async)? ?(\w+) ?=>/g; 68 | let paramCode = (rule.toString().split(')')[0] + ')'); 69 | let paramCodeWithoutEol = paramCode.replace(/\s+/g, " "); 70 | let allParamMatch = allParamsRegex.exec(paramCodeWithoutEol); 71 | if (!allParamMatch) return; 72 | 73 | let allParams = allParamMatch[1]; 74 | let paramMatch = paramRegex.exec(allParams); 75 | do { 76 | yield (paramMatch[1] || paramMatch[2]); 77 | paramMatch = paramRegex.exec(allParams); 78 | } 79 | while (paramMatch) 80 | } 81 | 82 | 83 | /** 84 | * Extracts the context from the fact or rule name. Must be a compiled path, not a path that 85 | * contains "../" or "./". 86 | * 87 | * @example 88 | * 89 | * Given the rule path: '/path/to/some/rule' 90 | * The returned value would be: '/path/to/some' 91 | * 92 | * @param {String} compiledPath The compiled path to extract the context from. 93 | */ 94 | function getContext(compiledPath) { 95 | return compiledPath.replace(trailingTermRemovalRegex, ""); 96 | } 97 | 98 | /** 99 | * Create a compacted fact name from a given fact name. Path do not end with a trailing '/'. 100 | * 101 | * '../' Denote the parent term. Move up one term in the context or path. 102 | * './' Denote the current term. Removed from the context or path. 103 | * 104 | * Examples: 105 | * 106 | * '/a/given/../correct/./path' is compiled to '/a/correct/path' 107 | * 108 | * @param {String} path The path or context to compile into a context. 109 | */ 110 | function compilePath(path) { 111 | let current = path; 112 | let next = path.replace(pathCompactionRegex, ""); 113 | while (next != current) { 114 | current = next; 115 | next = current.replace(pathCompactionRegex, ""); 116 | } 117 | if (current.startsWith("/..")) { 118 | throw new Error(`Unable to compile the path '${path}' properly.`); 119 | } 120 | return current.replace(currentPathRemoval, ""); 121 | } 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infernal-engine", 3 | "version": "1.1.2", 4 | "description": "Javascript Inference Engine", 5 | "keywords": [ 6 | "inference", 7 | "engine", 8 | "expert", 9 | "system", 10 | "artificial", 11 | "intelligence", 12 | "ai", 13 | "forward-chaining" 14 | ], 15 | "license": "MIT", 16 | "author": "Jean-Philippe Gravel ", 17 | "main": "lib/index.js", 18 | "homepage": "https://formix.github.io/infernal-engine/", 19 | "bugs": "https://github.com/formix/infernal-engine/issues", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/formix/infernal-engine.git" 23 | }, 24 | "scripts": { 25 | "test": "./node_modules/mocha/bin/_mocha", 26 | "dist": "webpack", 27 | "doc": "jsdoc -c ./jsdoc/conf.json -r ./lib ./README.md -d ./../infernal-engine-pages" 28 | }, 29 | "devDependencies": { 30 | "mocha": "^10.2.0", 31 | "nyc": "^15.1.0", 32 | "webpack": "^5.11.1", 33 | "webpack-cli": "^4.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/cli/tracing.js: -------------------------------------------------------------------------------- 1 | //const InfernalEngine = require("infernal-engine"); 2 | const InfernalEngine = require("../../lib/index"); 3 | const model = require("../models/critterModel"); 4 | 5 | (async () => { 6 | let engine = new InfernalEngine(null, 7 | msg => console.log("-> ", JSON.stringify(msg))); 8 | 9 | console.log("Importing the critterModel:"); 10 | await engine.import(model); 11 | 12 | console.log("Initial facts:") 13 | let initialModel = await engine.export(); 14 | console.log(JSON.stringify(initialModel, null, " ")); 15 | 16 | console.log("Importing two facts to be asserted:"); 17 | await engine.import({ 18 | sound: "croaks", 19 | eats: "flies" 20 | }) 21 | 22 | console.log("Inferred facts:") 23 | let inferredModel = await engine.export(); 24 | console.log(JSON.stringify(inferredModel, null, " ")); 25 | })(); 26 | -------------------------------------------------------------------------------- /test/models/carModel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | name: "Minivan", 4 | 5 | speed: { 6 | input: "0", 7 | limit: 140, 8 | value: 0, 9 | 10 | inputIsValidInteger: async function(input) { 11 | let isInt = /^-?\d+$/.test(input); 12 | if (!isInt) { 13 | return { "../message": `Error: '${input}' is not a valid integer.` } 14 | } 15 | return { 16 | "../message": undefined, 17 | value: Number(input) 18 | }; 19 | }, 20 | 21 | valueIsUnderLimit: async function(value, limit) { 22 | if (value > limit) { 23 | return { 24 | value: limit, 25 | "/message": `WARN: The speed input can not exceed the speed limit of ${limit}.` 26 | } 27 | } 28 | } 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /test/models/critterModel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | name: "Fritz", 4 | 5 | sound: "", 6 | eats: "", 7 | sings: false, 8 | 9 | color: "unknown", 10 | species: "unknown", 11 | 12 | isFrog: async function(sound, eats){ 13 | let species = "unknown"; 14 | if (sound === "croaks" && eats === "flies") { 15 | species = "frog"; 16 | } 17 | return {"species": species}; 18 | }, 19 | 20 | isCanary: async function(sound, sings) { 21 | if ( sings && sound === "chirps" ) { 22 | return {"species": "canary"}; 23 | } 24 | }, 25 | 26 | isGreen: async function(species) { 27 | if (species === "frog") { 28 | return {"color": "green"}; 29 | } 30 | }, 31 | 32 | isYellow: async function(species) { 33 | if (species === "canary") { 34 | return {"color": "yellow"}; 35 | } 36 | } 37 | 38 | }; -------------------------------------------------------------------------------- /test/test-engine.js: -------------------------------------------------------------------------------- 1 | const InfernalEngine = require("../lib/index"); 2 | const assert = require("assert"); 3 | 4 | 5 | describe("InfernalEngine", async() => { 6 | 7 | describe("#assert", async () => { 8 | let engine = new InfernalEngine(); 9 | it("asserting the value 'i' shall add a new fact '/i' in the engine.", async () => { 10 | let engine = new InfernalEngine(); 11 | await engine.assert("i", 5); 12 | assert.deepStrictEqual(engine._facts.has("/i"), true); 13 | assert.deepStrictEqual(engine._changes.size, 1); 14 | }); 15 | it("asserting the value '/i' shall change the xisting fact '/i' in the engine.", async () => { 16 | let engine = new InfernalEngine(); 17 | await engine.assert("/i", 0); 18 | assert.deepStrictEqual(engine._facts.get("/i"), 0); 19 | assert.deepStrictEqual(engine._changes.size, 1); 20 | }); 21 | }); 22 | 23 | describe("#assertAll", async () => { 24 | let engine = new InfernalEngine(); 25 | it("asserting '/a', '/b' and '/c/d' all at once shall create the matching model.'", async () => { 26 | let engine = new InfernalEngine(); 27 | await engine.assertAll([ 28 | InfernalEngine.fact("a", 1), 29 | InfernalEngine.fact("/b", 2), 30 | InfernalEngine.fact("/c/d", 3) 31 | ]); 32 | let model = await engine.export(); 33 | delete model.$; 34 | assert.deepStrictEqual(model, { 35 | a:1, b:2, c: { d: 3 } 36 | }); 37 | }); 38 | }); 39 | 40 | describe("#peek", () => { 41 | it("shall get an existing fact from the engine.", async () => { 42 | let engine = new InfernalEngine(); 43 | await engine.assert("i", 7, true); 44 | let i = await engine.peek("i"); 45 | assert.deepStrictEqual(i, 7); 46 | let i2 = await engine.peek("/i"); 47 | assert.deepStrictEqual(i2, 7); 48 | }); 49 | }); 50 | 51 | describe("#retract", () => { 52 | it("shall retract a single fact from the engine.", async () => { 53 | let engine = new InfernalEngine(); 54 | await engine.assert("i", 7, true); 55 | assert.deepStrictEqual(await engine.peek("i"), 7); 56 | await engine.retract("i"); 57 | assert.deepStrictEqual(await engine.peek("i"), undefined); 58 | }); 59 | 60 | it("shall retract all facts from the given path prefix.", async () => { 61 | let engine = new InfernalEngine(); 62 | let model = { 63 | a: "a", 64 | b: 1, 65 | c: true, 66 | d: { 67 | x: "23", 68 | y: 42, 69 | z: 5.5 70 | }, 71 | }; 72 | await engine.import(model); 73 | await engine.retract("/d/*"); 74 | let expectedFacts = { 75 | a: "a", 76 | b: 1, 77 | c: true 78 | }; 79 | let actualFacts = await engine.export(); 80 | delete actualFacts.$; 81 | assert.deepStrictEqual(actualFacts, expectedFacts); 82 | }); 83 | 84 | }); 85 | 86 | describe("#def", () => { 87 | it("shall add a rule and interpret the parameters correctly.", async () => { 88 | let engine = new InfernalEngine(); 89 | await engine.def("rule1", async function(i) {}); 90 | assert.deepStrictEqual(engine._rules.has("/rule1"), true, 91 | "The rule '/rule1' was not added to the internal ruleset."); 92 | assert.deepStrictEqual(engine._relations.get("/i").has("/rule1"), true, 93 | "The relation between the fact '/i' and the rule '/rule1' was not properly established."); 94 | 95 | await engine.def("s/rule", async function(i) {}); 96 | assert.deepStrictEqual(engine._rules.has("/s/rule"), true, 97 | "The rule '/s/rule' was not added to the internal ruleset."); 98 | assert.deepStrictEqual(engine._relations.get("/s/i").has("/s/rule"), true, 99 | "The relation between the fact '/s/i' and the rule '/s/rule' was not properly established."); 100 | }); 101 | 102 | it("shall add multiple fact-rule relations given multiple parameters.", async () => { 103 | let engine = new InfernalEngine(); 104 | await engine.def("rule", async function(i, a, b) {}); // multiple local facts 105 | assert.deepStrictEqual(engine._relations.get("/i").has("/rule"), true, 106 | "The relation between the fact '/i' and the rule '/rule' was not properly established."); 107 | assert.deepStrictEqual(engine._relations.get("/a").has("/rule"), true, 108 | "The relation between the fact '/a' and the rule '/rule' was not properly established."); 109 | assert.deepStrictEqual(engine._relations.get("/b").has("/rule"), true, 110 | "The relation between the fact '/b' and the rule '/rule' was not properly established."); 111 | }); 112 | 113 | it("shall add a rule referencing a fact with a specified path", async () => { 114 | let engine = new InfernalEngine(); 115 | await engine.def("rule", async function(/*@ /another/path */ x) {}); 116 | assert.deepStrictEqual(engine._relations.get("/another/path").has("/rule"), true, 117 | "The relation between the fact '/another/path' and the rule '/rule' was not properly established."); 118 | }); 119 | 120 | it("shall add a rule referencing a fact with a specified path for multiple parameters", async () => { 121 | let engine = new InfernalEngine(); 122 | await engine.def("rule", async function(/*@ /another/path */ x, /*@ /some/other/path */ y) {}); 123 | assert.deepStrictEqual(engine._relations.get("/another/path").has("/rule"), true, 124 | "The relation between the fact '/another/path' and the rule '/rule' was not properly established."); 125 | assert.deepStrictEqual(engine._relations.get("/some/other/path").has("/rule"), true, 126 | "The relation between the fact '/some/other/path' and the rule '/rule' was not properly established."); 127 | }); 128 | 129 | it("shall add a rule referencing a fact with a specified complex path", async () => { 130 | let engine = new InfernalEngine(); 131 | await engine.def("/a/another/path/rule", async function(/*@ ../.././some/./fact */ x) {}); 132 | assert.deepStrictEqual(engine._relations.get("/a/some/fact").has("/a/another/path/rule"), true, 133 | "The relation between the fact '/a/some/fact' and the rule '/a/another/path/rule' was not properly established."); 134 | }); 135 | 136 | }); 137 | 138 | describe("#undef", () => { 139 | 140 | it("shall not count up to 5 once the rule is undefined.", async () => { 141 | let engine = new InfernalEngine(); 142 | await engine.def("count5", async function(i) { 143 | if (typeof i !== "undefined" && i < 5) { 144 | return { "i": i + 1 }; 145 | } 146 | }); 147 | await engine.undef("count5"); 148 | await engine.assert("i", 1); 149 | let final_i = await engine.peek("i"); 150 | assert.deepStrictEqual(final_i, 1); 151 | }); 152 | 153 | it("shall undefine all rules from the carModel", async () => { 154 | let engine = new InfernalEngine(); 155 | let carModel = require("./models/carModel"); 156 | await engine.import(carModel); 157 | await engine.undef("/speed/*"); 158 | engine.reset(); 159 | await engine.assert("/speed/input", "invalid number"); 160 | let changes = await engine.exportChanges(); 161 | assert.deepStrictEqual(changes, { 162 | speed: { 163 | input: "invalid number" 164 | } 165 | }); 166 | }); 167 | 168 | }); 169 | 170 | describe("#infer", () => { 171 | 172 | it("shall count up to 5.", async () => { 173 | let engine = new InfernalEngine(); 174 | await engine.def("count5", async (i) => { 175 | if (typeof i !== "undefined" && i < 5) { 176 | return { "i": i + 1 }; 177 | } 178 | }); 179 | await engine.assert("i", 1); 180 | let final_i = await engine.peek("i"); 181 | assert.deepStrictEqual(final_i, 5); 182 | }); 183 | 184 | it("#assert.", async function() { 185 | let engine = new InfernalEngine(); 186 | await engine.def("count5", async function(i) { 187 | if (typeof i !== "undefined" && i < 7) { 188 | return { 189 | "#assert": { 190 | path: "i", 191 | value: i + 1 192 | } 193 | }; 194 | } 195 | }); 196 | await engine.assert("i", 4); 197 | let final_i = await engine.peek("i"); 198 | assert.deepStrictEqual(final_i, 7); 199 | }); 200 | 201 | it("#retract.", async () => { 202 | let engine = new InfernalEngine(); 203 | await engine.def("count7", async function(i) { 204 | if (typeof i !== "undefined" && i < 7) { 205 | return { 206 | "#assert": { 207 | path: "i", 208 | value: i + 1 209 | } 210 | }; 211 | } 212 | }); 213 | await engine.def("retract_i", async function(i) { 214 | return { 215 | "#retract": { 216 | path: "i" 217 | } 218 | } 219 | }); 220 | await engine.assert("i", 4); 221 | let final_i = await engine.peek("i"); 222 | assert.deepStrictEqual(final_i, undefined); 223 | }); 224 | 225 | it("#def", async () => { 226 | let engine = new InfernalEngine(); 227 | await engine.def("count5", async function(i, added) { 228 | if (typeof i === "undefined") return; 229 | if (i < 7) { 230 | return { 231 | "#assert": { 232 | path: "i", 233 | value: i + 1 234 | } 235 | }; 236 | } 237 | else if (i < 14) { 238 | return { 239 | "#def": { 240 | path: "mult2", 241 | value: async function(i) { 242 | return { 243 | "j": i * 2 244 | } 245 | } 246 | } 247 | } 248 | } 249 | }); 250 | await engine.assert("i", 4); 251 | let final_j = await engine.peek("j"); 252 | assert.deepStrictEqual(final_j, 14); 253 | }); 254 | 255 | it("#undef.", async () => { 256 | let engine = new InfernalEngine(); 257 | await engine.import({ 258 | count7: async function(i) { 259 | if (typeof i !== "undefined" && i < 7) { 260 | return { 261 | "#assert": { 262 | path: "i", 263 | value: i + 1 264 | } 265 | }; 266 | } 267 | }, 268 | undef: async function(i) { 269 | return { 270 | "#undef": { 271 | path: "count7" 272 | } 273 | } 274 | } 275 | }); 276 | await engine.assert("i", 4); 277 | let final_i = await engine.peek("i"); 278 | assert.deepStrictEqual(final_i, 4); 279 | }); 280 | 281 | it("#import", async () => { 282 | let engine = new InfernalEngine(); 283 | await engine.def("addCarModel", async function() { 284 | let carModel = require("./models/carModel"); 285 | return { 286 | "#import": { 287 | path: "car", 288 | value: carModel 289 | } 290 | } 291 | }); 292 | let carModelExported = await engine.export("/car"); 293 | delete carModelExported.$; 294 | assert.deepStrictEqual(carModelExported, { 295 | name: "Minivan", 296 | speed: { 297 | input: "0", 298 | limit: 140, 299 | value: 0 300 | } 301 | }); 302 | }); 303 | 304 | }); 305 | 306 | 307 | describe("#import", () => { 308 | 309 | it("shall load and infer the animal is agreen frog.", async () => { 310 | let engine = new InfernalEngine(); 311 | let critterModel = require("./models/critterModel"); 312 | await engine.import(critterModel); 313 | await engine.import({ 314 | eats: "flies", 315 | sound: "croaks" 316 | }); 317 | assert.deepStrictEqual(await engine.peek("species"), "frog"); 318 | assert.deepStrictEqual(await engine.peek("color"), "green"); 319 | }); 320 | 321 | it("shall load and infer the animal is a green frog inside the submodel.", async () => { 322 | let engine = new InfernalEngine(); 323 | let critterModel = require("./models/critterModel"); 324 | await engine.import(critterModel, "/the/critter/model"); 325 | await engine.assert("/the/critter/model/eats", "flies"); 326 | await engine.assert("/the/critter/model/sound", "croaks"); 327 | assert.deepStrictEqual(await engine.peek("/the/critter/model/species"), "frog"); 328 | assert.deepStrictEqual(await engine.peek("/the/critter/model/color"), "green"); 329 | }); 330 | 331 | }); 332 | 333 | 334 | describe("#export", () => { 335 | 336 | it("shall export the same model as the one imported.", async () => { 337 | let engine = new InfernalEngine(); 338 | let model = { 339 | a: "a", 340 | b: 1, 341 | c: true, 342 | d: { 343 | x: "23", 344 | y: 42, 345 | z: 5.5 346 | } 347 | } 348 | await engine.import(model); 349 | let model2 = await engine.export(); 350 | delete model2.$; // we don't want to deal with meta facts 351 | assert.deepStrictEqual(model2, model); 352 | }); 353 | 354 | it("shall export the same submodel as the one imported.", async () => { 355 | let engine = new InfernalEngine(); 356 | let model = { 357 | a: "a", 358 | b: 1, 359 | c: true, 360 | d: { 361 | x: "23", 362 | y: 42, 363 | z: false 364 | } 365 | } 366 | await engine.import(model); 367 | let model2 = await engine.export("/d"); 368 | assert.deepStrictEqual(model2, model.d); 369 | }); 370 | 371 | }); 372 | 373 | 374 | describe("#exportChanges", () => { 375 | 376 | it("shall export what changed during inference.", async () => { 377 | let engine = new InfernalEngine(); 378 | let carModel = require("./models/carModel"); 379 | await engine.import(carModel); 380 | engine.reset(); 381 | await engine.assert("/speed/input", "50"); 382 | let changes = await engine.exportChanges(); 383 | assert.deepStrictEqual(changes, { 384 | speed: { 385 | input: "50", 386 | value: 50 387 | } 388 | }); 389 | }); 390 | 391 | it("shall add a message at the root of the model.", async () => { 392 | let engine = new InfernalEngine(); 393 | let carModel = require("./models/carModel"); 394 | await engine.import(carModel); 395 | engine.reset(); 396 | await engine.assert("/speed/input", "invalid number"); 397 | let changes = await engine.exportChanges(); 398 | assert.deepStrictEqual(changes, { 399 | message: "Error: 'invalid number' is not a valid integer.", 400 | speed: { 401 | input: "invalid number" 402 | } 403 | }); 404 | }); 405 | 406 | it("shall do the conversion and add a message at the root of the model.", async () => { 407 | let engine = new InfernalEngine(); 408 | let carModel = require("./models/carModel"); 409 | await engine.import(carModel); 410 | engine.reset(); 411 | await engine.assert("/speed/input", "200"); 412 | let changes = await engine.exportChanges(); 413 | assert.deepStrictEqual(changes, { 414 | message: "WARN: The speed input can not exceed the speed limit of 140.", 415 | speed: { 416 | input: "200", 417 | value: 140 418 | } 419 | }); 420 | }); 421 | }); 422 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | "infernal-engine": "./lib/index.js", 7 | }, 8 | mode: "production", 9 | //mode: "development", 10 | devtool: "source-map", 11 | output: { 12 | path: path.resolve(__dirname, 'dist'), 13 | filename: "[name].js", 14 | libraryTarget: "umd", 15 | library: "InfernalEngine" 16 | } 17 | }; --------------------------------------------------------------------------------