Repository: CacheControl/json-rules-engine
Pull request #2 (2016-04-16 21:33:13)

Docs+tweaks
* Completed the 'fact-dependecy' advanced example
* Updated addFact and addRule engine methods to return 'this' for easy chaining


Patch:
diff --git a/.gitignore b/.gitignore
index 6edd850..c490e2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /node_modules
 npm-debug.log
+.vscode
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50b2528..2c10cf4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+1.0.0-beta10 / 2016-04-16
+==================
+
+  * Completed the 'fact-dependecy' advanced example
+  * Updated addFact and addRule engine methods to return 'this' for easy chaining
+
 1.0.0-beta9 / 2016-04-11
 ==================
 
diff --git a/README.md b/README.md
index 71baa29..b1eae1d 100644
--- a/README.md
+++ b/README.md
@@ -174,11 +174,12 @@ let ageFact = function(params, almanac) {
 engine.addFact('age', ageFact)
 ```
 
-Now when the engine is run, it will call the methods above whenever it encounters the ```fact: "age"``` or ```fact: "state"```properties.
+Now when the engine is run, it will call the methods above whenever it encounters the ```fact: "age"``` or ```fact: "state"``` properties.
 
 **Important:** facts should be *pure functions*; their computed values will vary based on the ```params``` argument.  By establishing facts as pure functions, it allows the rules engine to cache results throughout each ```run()```; facts called multiple times with the same ```params``` will trigger the computation once and cache the results for future calls.  If fact caching not desired, this behavior can be turned off via the options; see the [docs](./docs/facts.md).
 
 More on facts can be found [here](./docs/facts.md)
+More on almanacs can be found [here](./docs/almanac.md)
 
 
 ### Step 4: Handing Events
diff --git a/dist/almanac.js b/dist/almanac.js
index 936e5ba..fb4f14e 100644
--- a/dist/almanac.js
+++ b/dist/almanac.js
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
   value: true
 });
 
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
+
 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
 
 var _fact = require('./fact');
@@ -31,27 +33,63 @@ var Almanac = function () {
     _classCallCheck(this, Almanac);
 
     this.factMap = factMap;
-    this.runtimeFacts = new Map();
     this.factResultsCache = new Map();
 
     for (var factId in runtimeFacts) {
       var fact = new _fact2.default(factId, runtimeFacts[factId]);
-      this.factResultsCache.set(fact.id, fact.value);
-      this.runtimeFacts.set(fact.id, fact);
-      debug('almanac::constructor initialized runtime fact \'' + fact.id + '\' with \'' + fact.value + '\'');
+      this.factMap.set(fact.id, fact);
+      this._setFactValue(fact, {}, fact.value);
+      debug('almanac::constructor initialized runtime fact:' + fact.id + ' with ' + fact.value + '<' + _typeof(fact.value) + '>');
     }
   }
 
   /**
-   * Returns the value of a fact, based on the given parameters.  Utilizes the 'almanac' maintained
-   * by the engine, which cache's fact computations based on parameters provided
-   * @param  {string} factId - fact identifier
-   * @param  {Object} params - parameters to feed into the fact.  By default, these will also be used to compute the cache key
-   * @return {Promise} a promise which will resolve with the fact computation.
+   * Retrieve fact by id, raising an exception if it DNE
+   * @param  {String} factId
+   * @return {Fact}
    */
 
 
   _createClass(Almanac, [{
+    key: '_getFact',
+    value: function _getFact(factId) {
+      var fact = this.factMap.get(factId);
+      if (fact === undefined) {
+        throw new Error('Undefined fact: ' + factId);
+      }
+      return fact;
+    }
+
+    /**
+     * Sets the computed value of a fact
+     * @param {Fact} fact
+     * @param {Object} params - values for differentiating this fact value from others, used for cache key
+     * @param {Mixed} value - computed value
+     */
+
+  }, {
+    key: '_setFactValue',
+    value: function _setFactValue(fact, params, value) {
+      var cacheKey = fact.getCacheKey(params);
+      var factValue = Promise.resolve(value);
+      factValue.then(function (val) {
+        return debug('almanac::factValue fact:' + fact.id + ' calculated as: ' + JSON.stringify(val) + '<' + (typeof val === 'undefined' ? 'undefined' : _typeof(val)) + '>');
+      });
+      if (cacheKey) {
+        this.factResultsCache.set(cacheKey, factValue);
+      }
+      return factValue;
+    }
+
+    /**
+     * Returns the value of a fact, based on the given parameters.  Utilizes the 'almanac' maintained
+     * by the engine, which cache's fact computations based on parameters provided
+     * @param  {string} factId - fact identifier
+     * @param  {Object} params - parameters to feed into the fact.  By default, these will also be used to compute the cache key
+     * @return {Promise} a promise which will resolve with the fact computation.
+     */
+
+  }, {
     key: 'factValue',
     value: function () {
       var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(factId) {
@@ -61,47 +99,25 @@ var Almanac = function () {
           while (1) {
             switch (_context.prev = _context.next) {
               case 0:
-                fact = this.runtimeFacts.get(factId);
-
-                if (!(fact !== undefined)) {
-                  _context.next = 3;
-                  break;
-                }
-
-                return _context.abrupt('return', fact.value);
-
-              case 3:
-                fact = this.factMap.get(factId);
-
-                if (!(fact === undefined)) {
-                  _context.next = 6;
-                  break;
-                }
-
-                throw new Error('Undefined fact: ' + factId);
-
-              case 6:
+                fact = this._getFact(factId);
                 cacheKey = fact.getCacheKey(params);
                 cacheVal = cacheKey && this.factResultsCache.get(cacheKey);
 
                 if (!cacheVal) {
-                  _context.next = 11;
+                  _context.next = 6;
                   break;
                 }
 
-                debug('almanac::factValue cache hit for fact:' + factId + ' cacheKey:' + cacheKey);
+                cacheVal.then(function (val) {
+                  return debug('almanac::factValue cache hit for fact:' + factId + ' cacheKey:' + cacheKey + ' value: ' + JSON.stringify(val) + '<' + (typeof val === 'undefined' ? 'undefined' : _typeof(val)) + '>');
+                });
                 return _context.abrupt('return', cacheVal);
 
-              case 11:
+              case 6:
                 debug('almanac::factValue cache miss for fact:' + factId + ' using cacheKey:' + cacheKey + '; calculating');
-                cacheVal = fact.calculate(params, this);
-                debug('almanac::factValue fact:' + factId + ' calculated as: ' + cacheVal);
-                if (cacheKey) {
-                  this.factResultsCache.set(cacheKey, cacheVal);
-                }
-                return _context.abrupt('return', cacheVal);
+                return _context.abrupt('return', this._setFactValue(fact, params, fact.calculate(params, this)));
 
-              case 16:
+              case 8:
               case 'end':
                 return _context.stop();
             }
diff --git a/dist/engine.js b/dist/engine.js
index 02c69a5..bffdee6 100644
--- a/dist/engine.js
+++ b/dist/engine.js
@@ -57,7 +57,7 @@ var Engine = function (_EventEmitter) {
     var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Engine).call(this));
 
     _this.rules = [];
-    rules.forEach(function (r) {
+    rules.map(function (r) {
       return _this.addRule(r);
     });
     _this.facts = new Map();
@@ -91,6 +91,7 @@ var Engine = function (_EventEmitter) {
 
       this.rules.push(rule);
       this.prioritizedRules = null;
+      return this;
     }
 
     /**
@@ -113,6 +114,7 @@ var Engine = function (_EventEmitter) {
       }
       debug('engine::addFact id:' + factId);
       this.facts.set(factId, fact);
+      return this;
     }
 
     /**
diff --git a/docs/almanac.md b/docs/almanac.md
new file mode 100644
index 0000000..0386016
--- /dev/null
+++ b/docs/almanac.md
@@ -0,0 +1,104 @@
+# Almanac
+
+## Overview
+
+An almanac collects facts through an engine run cycle.  As the engine computes fact values,
+the results are stored in the almanac and cache'd.  If the engine detects a fact computation has
+been previously computed, it re-uses the cache'd result from the almanac.  Every time ```engine.run()``` is invoked,
+a new almanac is instantiated.
+
+The almanac for the current engine run is available as arguments passed to the fact evaluation methods and
+ to the engine ```success``` event.  The almanac may be used to define additional facts during runtime.
+
+## Methods
+
+### almanac.factValue(Fact fact, Object params) -> Promise
+
+Computes the value of the provided fact + params.
+
+```js
+almanac
+  .factValue('account-information', { accountId: 1 })
+  .then( values => console.log(values))
+```
+
+## Common Use Cases
+
+### Fact dependencies
+
+The most common use of the almanac is to access data computed by other facts during runtime.  This allows
+leveraging the engine's caching mechanisms to design more efficient rules.
+
+The [computed-facts](../examples/computed-facts) example demonstrates a real world application of this technique.
+
+For example, say there were two facts: _is-funded-account_ and _account-balance_.  Both facts depend on the same _account-information_ data set.
+Using the Almanac, each fact can be defined to call a **base** fact responsible for loading the data.  This causes the engine
+to make the API call for loading account information only once per account.
+
+```js
+/*
+ * Base fact for retrieving account data information.
+ * Engine will automatically cache results by accountId
+ */
+let accountInformation = new Fact('account-information', function(params, almanac) {
+   return request
+      .get({ url: `http://my-service/account/${params.accountId}`)
+      .then(function (response) {
+         return response.data
+      })
+})
+
+/*
+ * Calls the account-information fact with the appropriate accountId.
+ * Receives a promise w/results unique to the accountId
+ */
+let isFundedAccount = new Fact('is-funded-account', function(params, almanac) {
+   return almanac.factValue('account-information', { accountId: params.accountId }).then(info => {
+     return info.funded === true
+   })
+})
+
+/*
+ * Calls the account-information fact with the appropriate accountId.
+ * Receives a promise w/results unique to the accountId
+ */
+let accountBalance = new Fact('account-balance', function(params, almanac) {
+   return almanac.factValue('account-information', { accountId: params.accountId }).then(info => {
+     return info.balance
+   })
+})
+
+/*
+ * Add the facts the the engine
+ */
+engine.addFact(accountInformation)
+engine.addFact(isFundedAccount)
+engine.addFact(accountBalance)
+
+/*
+ * Run the engine.
+ * account-information will be loaded ONCE for the account, regardless of how many
+ * times is-funded-account or account-balance are mentioned in the rules
+ */
+
+engine.run({ accountId: 1 })
+```
+
+### Retrieve fact values when handling events
+
+When a rule evalutes truthy and its ```event``` is called, new facts may be defined by the event handler.
+  Note that with this technique, the rule priority becomes important; if a rule is expected to
+  define a fact value, it's important that rule be run prior to other rules that reference the fact.  To
+  learn more about setting rule priorties, see the [rule documentation](./rule.md).
+
+```js
+engine.on('success', (event, almanac) => {
+  // Handle the event using a fact value to process
+  // Retrieve user's account info and make an api call using the results
+  almanac
+    .factValue('account-information', event.params.accountId)
+    .then(info => {
+      return request.post({ url: `http://my-service/toggle?funded=${!info.funded}`)
+    })
+})
+```
\ No newline at end of file
diff --git a/docs/engine.md b/docs/engine.md
index 6e4c3b6..3daad70 100644
--- a/docs/engine.md
+++ b/docs/engine.md
@@ -99,7 +99,6 @@ Fires when *any* rule passes.  In this case the callback will receive the entire
 
 ```js
 engine.on('success', function(event, almanac) {
-  console.log(event) // { type: 'my-event', params: { id: 1 } }
 })
 ```
 
@@ -108,7 +107,6 @@ engine.on('success', function(event, almanac) {
 Companion to 'success', except fires when any rule fails.
 
 ```js
-engine.on('failure', function(event, almanac) {
-  console.log(event) // { type: 'my-event', params: { id: 1 } }
+engine.on('failure', function(rule, almanac) {
 })
 ```
diff --git a/examples/basic.js b/examples/basic.js
index f35d3a3..d0ce2c9 100644
--- a/examples/basic.js
+++ b/examples/basic.js
@@ -5,10 +5,13 @@
  * two different sets of data.
  *
  * In this example, Facts are passed to run() as constants known at runtime.  For a more
- * complex example demonstrating computed facts, see the computed-facts example.
+ * complex example demonstrating computed facts, see the fact-dependency example.
  *
  * Usage:
  *   node ./examples/basic.js
+ *
+ * For detailed output:
+ *   DEBUG=json-rules-engine node ./examples/fact-dependency.js
  */
 
 require('babel-polyfill')
@@ -63,10 +66,10 @@ engine.addRule(educationRule)
  */
 var facts
 engine
-  .on('success', function (event, almanac) {
+  .on('success', function (event) {
     console.log(facts.username + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule. params: ' + JSON.stringify(event.params))
   })
-  .on('failure', function (rule, almanac) {
+  .on('failure', function (rule) {
     console.log(facts.username + ' did ' + 'NOT'.red + ' meet conditions for the ' + rule.event.type.underline + ' rule.')
   })
 
@@ -90,3 +93,12 @@ engine
     return engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run
   })
   .catch(console.log)
+
+/*
+ * OUTPUT:
+ *
+ * washington DID meet conditions for the income rule
+ * washington did NOT meet conditions for the education rule
+ * jefferson did NOT meet conditions for the income rule
+ * jefferson DID meet conditions for the education rule
+ */
diff --git a/examples/fact-dependecy.js b/examples/fact-dependecy.js
new file mode 100644
index 0000000..15dc82b
--- /dev/null
+++ b/examples/fact-dependecy.js
@@ -0,0 +1,153 @@
+'use strict'
+
+/*
+ * This is an advanced example that demonstrates facts with dependencies
+ * on other facts.  In addition, it demonstrates facts that load data asynchronously
+ * from outside sources (api's, databases, etc)
+ *
+ * Usage:
+ *   node ./examples/fact-dependency.js
+ *
+ * For detailed output:
+ *   DEBUG=json-rules-engine node ./examples/fact-dependency.js
+ */
+
+require('babel-polyfill')
+require('colors')
+var Engine = require('../dist').Engine
+var accountClient = require('./support/account-api-client')
+
+/**
+ * Setup a new engine
+ */
+var engine = new Engine()
+
+/**
+ * Rule for identifying microsoft employees that have been terminated.
+ * - Demonstates re-using a same fact with different parameters
+ * - Demonstates calling a base fact, which serves to load data once and reuse later
+ */
+var microsoftRule = {
+  conditions: {
+    all: [{
+      fact: 'account-information-field',
+      operator: 'equal',
+      value: 'microsoft',
+      params: {
+        field: 'company'
+      }
+    }, {
+      fact: 'account-information-field',
+      operator: 'equal',
+      value: 'terminated',
+      params: {
+        field: 'status'
+      }
+    }]
+  },
+  event: { type: 'microsoft-terminated-employees' }
+}
+engine.addRule(microsoftRule)
+
+/**
+ * Rule for identifying accounts older than 5 years
+ * - Demonstates calling a base fact, also shared by the account-information-field fact
+ * - Demonstates performing computations on data retrieved by base fact
+ */
+var tenureRule = {
+  conditions: {
+    all: [{
+      fact: 'account-tenure',
+      operator: 'greaterThanInclusive',
+      value: 5
+    }]
+  },
+  event: { type: 'five-year-tenure' }
+}
+engine.addRule(tenureRule)
+
+/**
+ * Register listeners with the engine for rule success and failure
+ */
+var facts
+engine
+  .on('success', function (event) {
+    console.log(facts.accountId + ' DID '.green + 'meet conditions for the ' + event.type.underline + ' rule.')
+  })
+  .on('failure', function (rule) {
+    console.log(facts.accountId + ' did ' + 'NOT'.red + ' meet conditions for the ' + rule.event.type.underline + ' rule.')
+  })
+
+/**
+ * 'account-information' fact executes an api call and retrieves account data
+ * - Demonstates facts called only by other facts and never mentioned directly in a rule
+ */
+engine.addFact('account-information', function (params, almanac) {
+  return accountClient.getAccountInformation(params.accountId)
+    .then(function (results) {
+      return results.data
+    })
+})
+
+/**
+ * 'account-tenure' fact retrieves account-information, and computes the number of years
+ * since the account was created using 'accountInformation.createdAt'
+ */
+engine.addFact('account-tenure', function (params, almanac) {
+  return almanac
+    .factValue('accountId')
+    .then(function (account) {
+      return almanac.factValue('account-information', { accountId: account })
+    })
+    .then(function (accountInformation) {
+      var created = new Date(accountInformation.createdAt)
+      var now = new Date()
+      return now.getFullYear() - created.getFullYear()
+    })
+    .catch(console.log)
+})
+
+/**
+ * 'account-information-field' fact returns any field from account-information for operator comparison
+ */
+engine.addFact('account-information-field', function (params, almanac) {
+  return almanac
+    .factValue('accountId')
+    .then(function (account) {
+      return almanac.factValue('account-information', { accountId: account })
+    })
+    .then(function (accountInformation) {
+      return accountInformation[params.field]  // return the specific field value
+    })
+    .catch(console.log)
+})
+
+// define fact(s) known at runtime
+facts = { accountId: 'washington' }
+engine
+  .run(facts)  // first run, using washington's facts
+  .then(function () {
+    facts = { accountId: 'jefferson' }
+    return engine.run(facts) // second run, using jefferson's facts; facts & evaluation are independent of the first run
+  })
+  .catch(console.log)
+
+/*
+ * OUTPUT:
+ *
+ * loading account information for "washington"
+ * washington DID meet conditions for the microsoft-terminated-employees rule.
+ * washington did NOT meet conditions for the five-year-tenure rule.
+ * loading account information for "jefferson"
+ * jefferson did NOT meet conditions for the microsoft-terminated-employees rule.
+ * jefferson DID meet conditions for the five-year-tenure rule.
+ */
+
+/*
+ * NOTES:
+ *
+ * - Notice that although a total of 6 conditions were evaluated using
+ *   account-information (3 rule conditions x 2 accounts), the account-information api call
+ *   is only called twice -- once for each account.  This is due to the base fact caching the results
+ *   for washington and jefferson after the initial data load.
+ */
diff --git a/examples/support/account-api-client.js b/examples/support/account-api-client.js
new file mode 100644
index 0000000..bec75e6
--- /dev/null
+++ b/examples/support/account-api-client.js
@@ -0,0 +1,31 @@
+'use strict'
+
+require('colors')
+
+var accountData = {
+  washington: {
+    company: 'microsoft',
+    status: 'terminated',
+    createdAt: '2012-02-14'
+  },
+  jefferson: {
+    company: 'apple',
+    status: 'terminated',
+    createdAt: '2005-04-03'
+  }
+}
+
+/**
+ * mock api client for retrieving account information
+ */
+module.exports = {
+  getAccountInformation: function (accountId) {
+    var message = 'loading account information for "' + accountId + '"'
+    console.log(message.dim)
+    return new Promise(function (resolve, reject) {
+      setImmediate(function () {
+        resolve({ status: 'success', data: accountData[accountId] })
+      })
+    })
+  }
+}
diff --git a/package.json b/package.json
index 9acfaf2..4483b31 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "json-rules-engine",
-  "version": "1.0.0-beta9",
+  "version": "1.0.0-beta10",
   "description": "Rules Engine expressed in simple json",
   "main": "dist/index.js",
   "scripts": {
diff --git a/src/almanac.js b/src/almanac.js
index 95395f9..f3a18d8 100644
--- a/src/almanac.js
+++ b/src/almanac.js
@@ -12,17 +12,45 @@ import Fact from './fact'
 export default class Almanac {
   constructor (factMap, runtimeFacts = new Map()) {
     this.factMap = factMap
-    this.runtimeFacts = new Map()
     this.factResultsCache = new Map()
 
     for (let factId in runtimeFacts) {
       let fact = new Fact(factId, runtimeFacts[factId])
-      this.factResultsCache.set(fact.id, fact.value)
-      this.runtimeFacts.set(fact.id, fact)
-      debug(`almanac::constructor initialized runtime fact '${fact.id}' with '${fact.value}'`)
+      this.factMap.set(fact.id, fact)
+      this._setFactValue(fact, {}, fact.value)
+      debug(`almanac::constructor initialized runtime fact:${fact.id} with ${fact.value}<${typeof fact.value}>`)
     }
   }
 
+  /**
+   * Retrieve fact by id, raising an exception if it DNE
+   * @param  {String} factId
+   * @return {Fact}
+   */
+  _getFact (factId) {
+    let fact = this.factMap.get(factId)
+    if (fact === undefined) {
+      throw new Error(`Undefined fact: ${factId}`)
+    }
+    return fact
+  }
+
+  /**
+   * Sets the computed value of a fact
+   * @param {Fact} fact
+   * @param {Object} params - values for differentiating this fact value from others, used for cache key
+   * @param {Mixed} value - computed value
+   */
+  _setFactValue (fact, params, value) {
+    let cacheKey = fact.getCacheKey(params)
+    let factValue = Promise.resolve(value)
+    factValue.then(val => debug(`almanac::factValue fact:${fact.id} calculated as: ${JSON.stringify(val)}<${typeof val}>`))
+    if (cacheKey) {
+      this.factResultsCache.set(cacheKey, factValue)
+    }
+    return factValue
+  }
+
   /**
    * Returns the value of a fact, based on the given parameters.  Utilizes the 'almanac' maintained
    * by the engine, which cache's fact computations based on parameters provided
@@ -31,26 +59,14 @@ export default class Almanac {
    * @return {Promise} a promise which will resolve with the fact computation.
    */
   async factValue (factId, params = {}) {
-    let fact = this.runtimeFacts.get(factId)
-    if (fact !== undefined) {
-      return fact.value
-    }
-    fact = this.factMap.get(factId)
-    if (fact === undefined) {
-      throw new Error(`Undefined fact: ${factId}`)
-    }
+    let fact = this._getFact(factId)
     let cacheKey = fact.getCacheKey(params)
     let cacheVal = cacheKey && this.factResultsCache.get(cacheKey)
     if (cacheVal) {
-      debug(`almanac::factValue cache hit for fact:${factId} cacheKey:${cacheKey}`)
+      cacheVal.then(val => debug(`almanac::factValue cache hit for fact:${factId} cacheKey:${cacheKey} value: ${JSON.stringify(val)}<${typeof val}>`))
       return cacheVal
     }
     debug(`almanac::factValue cache miss for fact:${factId} using cacheKey:${cacheKey}; calculating`)
-    cacheVal = fact.calculate(params, this)
-    debug(`almanac::factValue fact:${factId} calculated as: ${cacheVal}`)
-    if (cacheKey) {
-      this.factResultsCache.set(cacheKey, cacheVal)
-    }
-    return cacheVal
+    return this._setFactValue(fact, params, fact.calculate(params, this))
   }
 }
diff --git a/src/engine.js b/src/engine.js
index b1e947a..d359868 100644
--- a/src/engine.js
+++ b/src/engine.js
@@ -20,7 +20,7 @@ class Engine extends EventEmitter {
   constructor (rules = []) {
     super()
     this.rules = []
-    rules.forEach((r) => this.addRule(r))
+    rules.map(r => this.addRule(r))
     this.facts = new Map()
     this.status = READY
   }
@@ -47,6 +47,7 @@ class Engine extends EventEmitter {
 
     this.rules.push(rule)
     this.prioritizedRules = null
+    return this
   }
 
   /**
@@ -66,6 +67,7 @@ class Engine extends EventEmitter {
     }
     debug(`engine::addFact id:${factId}`)
     this.facts.set(factId, fact)
+    return this
   }
 
   /**


Test patch:
diff --git a/test/almanac.test.js b/test/almanac.test.js
index d0beef5..1a5ea56 100644
--- a/test/almanac.test.js
+++ b/test/almanac.test.js
@@ -13,8 +13,8 @@ describe('Almanac', () => {
     })
 
     it('adds runtime facts', () => {
-      almanac = new Almanac({}, { modelId: 'XYZ' })
-      expect(almanac.runtimeFacts.get('modelId').value).to.equal('XYZ')
+      almanac = new Almanac(new Map(), { modelId: 'XYZ' })
+      expect(almanac.factMap.get('modelId').value).to.equal('XYZ')
     })
   })
 
@@ -43,7 +43,53 @@ describe('Almanac', () => {
       })
     })
 
-    describe('fact caching', () => {
+    describe('_getFact', _ => {
+      it('retrieves the fact object', () => {
+        let facts = new Map()
+        let fact = new Fact('id', 1)
+        facts.set(fact.id, fact)
+        almanac = new Almanac(facts)
+        expect(almanac._getFact('id')).to.equal(fact)
+      })
+
+      it('raises an exception if fact DNE', () => {
+        almanac = new Almanac(new Map())
+        expect(almanac._getFact.bind(almanac, 'unknown')).to.throw(/Undefined fact/)
+      })
+    })
+
+    describe('_setFactValue()', () => {
+      function expectFactResultsCache (expected) {
+        let promise = almanac.factResultsCache.values().next().value
+        expect(promise).to.be.instanceof(Promise)
+        promise.then(value => expect(value).to.equal(expected))
+        return promise
+      }
+
+      function setup (f = new Fact('id', 1)) {
+        fact = f
+        let facts = new Map()
+        facts.set(fact.id, fact)
+        almanac = new Almanac(facts)
+      }
+      let fact
+      const FACT_VALUE = 2
+
+      it('updates the fact results and returns a promise', (done) => {
+        setup()
+        almanac._setFactValue(fact, {}, FACT_VALUE)
+        expectFactResultsCache(FACT_VALUE).then(_ => done()).catch(done)
+      })
+
+      it('honors facts with caching disabled', (done) => {
+        setup(new Fact('id', 1, { cache: false }))
+        let promise = almanac._setFactValue(fact, {}, FACT_VALUE)
+        expect(almanac.factResultsCache.values().next().value).to.be.undefined
+        promise.then(value => expect(value).to.equal(FACT_VALUE)).then(_ => done()).catch(done)
+      })
+    })
+
+    describe('factValue()', () => {
       function setup (factOptions) {
         factSpy.reset()
         let fact = new Fact('foo', async (params, facts) => {