├── .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 | [](https://gitter.im/formix/infernal-engine?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](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 | };
--------------------------------------------------------------------------------