EXPORTS_OR_THIS = (exports ? this)DataReducers: Min(@alice) the minimum value from @alice etc: Min|Max|Average|Earliest|Latest
Latest(everyone) the most recent expressed value from anyone
Min(@alice) or Max(everyone) The lowest value alice has ever expressed or the highest value anyone else has.
Min(@alice,@bob) or Min(@carol) or Max(everyone) The lowest value either alice or bob have expressed or the lowest value carol has expressed or the highest value anyone else has expressed.
Min( Latest(@alice,@bob) or Latest(@carol) ) The lesser of either alice or bob’s most recently asserted value OR the most recently asserted value by carol.
Avg( Min( Latest(@alice or @bob)) or Max( Earliest(@carol or @dan) ) ) We consider the latest value from alice and the latest from bob. We consider the earliest from carol and the earliest from dan. Return the average of the minimum of the latest from alice OR the latest from bob and the maximum of the latest from carol OR the latest from dan.
 Notice that this is as deep as a DataReducer can get.
 Why?
 Alice and Bob are an "equivilence class" -- meaning that evaluations
 from either of them are treated as being interchangeable.
Avg( 6Min(@alice) or 4Min(@bob) or Min(@carol) ) Avg( Min(6@alice or 6@bob or @carol) ) Return a value which weighs the minimum value from alice as 5 times the minimum value from bob which is 3 times the minimum value from carol as the unit value. eg Min(@alice) is 10 Min(@bob) is 20 Min(@carol) is 25 Return (610 + 420 + 25)/(6+4+1) ==> (60 + 80 + 25) / 11 ==> 15
   Q1. Or is each tier (equivalence class) N-times greater than the next?
       return (6*4*10 + 4*20 + 25)/((6*4)+4+1) ==> (240 + 80 + 25)/29 => 345/29 => 11.89
       The advantage of this approach is that one can keep adding equivalence classes
       and not have to 'rebalance the weightings'.  Rather, the spirit of successively
       dominating equivalenct classes seems to be more along the lines of multiplicative
       weighting than additive weighting -- if that makes any sense.
Latest(Min(@alice,@bob or @carol)) # ‘or’ replaces ‘>’ to mean “dominates”
Latest(Min(@alice,@bob,@carol) or Max(everyone))
-- one to apply to each individual
   and one to apply across the participating individuals.
A DataReduction is a ‘reduced data set’ after WhoToHeed and Aggregation and Weighting.
Another way to think of this name is the product of “data reduction” process. Perhaps it should be called KnowledgeReduction. What Nooron needs is a StreamKnowledgeReducer which processes a stream of spogi and produces a changing “reduced value”.
WhoToHeed() eg: me, ‘betty’, ‘bob’ > ‘alice’, ‘cathy’ > everyone - ‘goofy’
Where each set of sources delimited by a > is an “equivalency class” meaning that if an allegation is provided by anybody in that set (eg “‘alice’, ‘cathy’”) then they dominate any “lesser” sources but because they are equivalent should have their alleged values aggregated
Aggregation()
Aggregation techniques are either Electors or Digestors. Electors elect one of the things which go in as the source of the output value.
suitable for: both object-values and literal-values
eg: First(), Latest(), Mode()
Digestors digest all of the things which go in to produce an output value.
suitable for: a subset of literal-values
They are suitable for numeric/linear/sortable data like numbers and datetimes.
eg: Average() [aka Mean()], Max(), Min()
problems: some literals can not be meaningfully Averaged, eg strings
Problem: all of the examples above retain the same range and units but Sum() and other aggregation functions like StdDev(), Variance() have different units and ranges, so though they are required perhaps those are best dealt with by “Functional Predicates” (aka “Computed Columns”).
Weighting() Weight([5,2,1] # a list of vote multipliers for the equivalency classes eg: “me/betty/bob allegations are worth 5x, alice/cathy are 2x from everyone” Problem: there are plausibly multiple algorithms Priority: Lower than simple Aggregation and even “Computed Columns”
There are various terms for different kinds of data that might be aggregated.
Problems:
EXPORTS_OR_THIS = (exports ? this)set a flag to indicate support for ES6-style Unicode support in RegExp
try new RegExp('','u') and UNICODE_REGEXP = true catch e then UNICODE_REGEXP = false
JSON_pps = (obj) ->
  JSON.stringify(obj, null, 4)
JSON_pp = (obj) ->
  console.log(JSON_pps(obj))equiv is an integer indicating which “equivalence class” the author of the spogi was found to be in. If equiv is 0 that means they were either explicitly ignored (eg “ - ‘bob’”) or not included (for example by there being no everyone term). Examples: WhoToHeed(“‘alice’ > ‘bob’ > everyone - ‘cathy’”).heed(‘cathy’) ==> 0 WhoToHeed(“‘alice’ > ‘bob’ > everyone - ‘cathy’”).heed(‘alice’) ==> 1 WhoToHeed(“‘alice’ > ‘bob’ > everyone - ‘cathy’”).heed(‘bob’) ==> 2 WhoToHeed(“‘alice’ > ‘bob’ > everyone - ‘cathy’”).heed(‘newton’) ==> 3 WhoToHeed(“‘alice’ > ‘bob’”).heed(‘cathy’) ==> 0
GENERIC = {}Provide a way to override the per-type (WPA|WCA|ACA) versions of Aggregator classes
WPA = WCA = ACA = GENERIC
class DataReducer
  @docs = "A DataReducer generically works across subjects and/or predicates"
  constructor: (dr_spec_or_ast, @me_is) ->dr_spec_or_ast ?= “Latest(Latest(Latest(everyone)))”
    discr_pth = (not window? and '../lib/' or '') + 'discriminator'
    DiscriminatorReader = require(discr_pth).DiscriminatorReader
    if typeof dr_spec_or_ast is 'string'
      @dd_spec = dr_spec_or_ast
      @dd_ast = new DiscriminatorReader(dr_spec_or_ast).ast.prog[0]
    else
      if typeof dr_spec_or_ast is 'object'
        @dd_ast = dr_spec_or_ast
      else
        throw new Error("new DataReducer() was passed neither a string nor an AST")
    @build_from_ast(@dd_ast)
  build_from_ast: (ast) ->
    if not (ast.type is 'call')
      console.log("ast:",JSON_pps(ast))
      throw new Error("build_from_ast() did not receive a well-formed Discriminator")
    @u_to_aggregator = {}
    @root = {}
    @make_branches(@root, ast)
    if ast.method?
      @re_predicates = ast.method.args.slice() # shallow copy
    else
      @re_predicates = []@re_predicates = [true]
  make_branches: (parent, ast) ->
    switch ast.type
      when 'call'
        this_node =
          fn_name: ast.func.value
          up: (not window?) and 'parent' or parent # to avoid circularity for JSON.stringify
        if not parent.kids?
          parent.kids = []
        parent.kids.push(this_node)
        for arg in ast.args
          if arg.type in ['atuser','genuser']
            this_node.heed = ast.args # bail and send whole heed AST
            break
          @make_branches(this_node, arg)when ‘atuser’ user = arg.value @u_to_aggregator[user] = parent when ‘genuser’ switch ast.value when ‘everyone’ @u_to_aggregator[EVERYONE] = parent when ‘others’ @u_to_aggregator[OTHERS] = parent when ‘me’ @u_to_aggregator[ME] = parent
  spawn: (s_p_key) ->
    return new DataReduction(@)See http://shapecatcher.com/ to find unicode by drawing them. http://shapecatcher.com/unicode/block/Arrows
  symbol_to_name:
    "↡":
      name: "Min"
      hex: '21A1'
    "↟":
      name: "Max"
      hex: '219F'
    "⏦":
      name: "Average"
      hex: '23E6'
    "↺":
      name: "Earliest"
      hex: '21BA'
    "↻":
      name: "Latest"
      hex: '21BB'
    "∀":
      name: "Earliest"
      hex: '2200'
  support_symbolic_aggr_names: UNICODE_REGEXP
  eg_src: "avg(max(latest(@alice|everyone~@bob)),earliest(me)).re(naughty,nice)"
  eg_json: [] # see docs/branched_and_complex_discriminator.json
class GenericAggregator
  constructor: () ->
    @members = {}
    @value = null
  init_reduction: () ->
    console.log @constructor.name,"init_reduction() should be removed"
  init_member: (member_id) ->
    @members[member_id] =
      id: member_id
  ensure_member: (member_id) ->console.log “ensuring member”, member_id
    return @members[member_id] or @init_member(member_id)
  cascade: (spogi, from_inner) ->call the HeedElector and recursively to the final aggregator
    rollup = @
    member = @consume(spogi, rollup, from_inner)
    if not member
      return member
    if @outer_aggr?
      return @outer_aggr.cascade(spogi, member)
    return member
  consume: (spogi, rollup, member) ->Return either false or the member whose aggregation was affected by the spogi
    @caused_promotion(spogi, rollup, member)
  caused_promotion: (spogi, rollup, member) ->
    if not rollup.value? or @should_promote(spogi, rollup, member)
      @promote_member(spogi, rollup, member)
      return @
    return false
  promote_spogi: (spogi, rollup, member) ->
    rollup.spogi_prior = rollup.spogi
    rollup.spogi = member.spogi
  promote_value: (spogi, rollup, member) ->
    rollup.value_prior = rollup.value
    rollup.value = member.value
  should_promote: (spogi, rollup, member) ->
    console.log @constructor.name,"should_elect() must be implemented"
class DataReduction
  constructor: (@data_reducer) ->the @members become the equivalency classes the member_id is a 1-based integer index
    @user_to_aggr = {}
    @value = null
    @compile(@data_reducer.root.kids[0], @)
  cascade: (spogi, inner_aggr) ->
    @spogi = inner_aggr.spogi
    @value = inner_aggr.value
    return truereturn @spogi
ReductionDispatcher.compile()
  compile: (abstract_branch, outer_aggr) ->
    if abstract_branch.fn_name
      this_aggr = new GENERIC[abstract_branch.fn_name]()
      if outer_aggr? and outer_aggr.constructor.name isnt 'DataReducer'
        this_aggr.outer_aggr = outer_aggr
      if abstract_branch.kids?
        for abstract_kid in abstract_branch.kids
          @compile(abstract_kid, this_aggr)
      if abstract_branch.heed?
        tiers = WhoToHeed.build_tiers_from_args(
              abstract_branch.heed, @data_reducer.me_is)
        he = new HeedElector()
        he.who_to_heed = new WhoToHeed(tiers, @data_reducer.me_is)
        he.test_for_domination = true
        he.outer_aggr = this_aggr
        for incl_u of tiers.incl_all # will be userid OR true (for everyone)
          if not @user_to_aggr[incl_u]?
            @user_to_aggr[incl_u] = []
          if incl_u is true
            throw new Error("incl_u should never be true, rather _everyone, _others, _notme or @WHATEVER")
          @user_to_aggr[incl_u].push(he)
  get_aggregators_for_user: (u_key) ->
    retlist = []. # TODO handle _other correctly
      concat(@user_to_aggr[u_key] or []).
      concat(@user_to_aggr[EVERYONE] or [])return retlist if false
    me_is = @data_reducer.me_is
    if not me_is?
      return []throw new Error(“me_is should be defined”)
    if not @user_to_aggr[u_key]? # ie u_key is not mentioned explicitly
      retlist = retlist.concat(@user_to_aggr[OTHERS] or [])
    if u_key isnt me_is # if u_key isnt the current user they are OTHERS
      retlist = retlist.concat(@user_to_aggr[NOTME] or [])
    return Array.from(new Set(retlist)) # uniqueify
  consume_spogi: (spogi) ->
    u_key = spogi.ww().user_symbolIterate through the possibly multiple entry points in the aggregation tree matching u_key (via explicit include, everyone, me or others)
    last_truthy_result = false
    for aggr, idx in @get_aggregators_for_user(u_key)
      spogi_or_false = aggr.cascade(spogi)
      if spogi_or_false
        last_truthy_result = spogi_or_falsereturn false OR the LAST truthy_result because it accumulates all changes
    return last_truthy_result
class HeedElector extends GenericAggregator # TODO rename to ReductionDispatcherExpose the GenericAggregator interface but just process WhoToHeed In other words, this is the innermost aggregation method and its sole purpose is to determine whether the spogi should even be processed based on WhoToHeed logic.
Actually, “sole purpose” is wrong. If ACA is happening then the a check for whether equiv_class_index1 is_dominated by a prior value should occur. If ACA IS NOT happening then there is no need for is_dominated testing.
  consume: (spogi, rollup_IGNORED, member_IGNORED) ->rollup and member are ignored because this is the first consume() called
    usym = spogi.ww().user_symbol
    if not usym
      throw new Error("expect user_symbol to be truthy", usym)
    equiv_class_index1 = @who_to_heed.heed(usym)
    if equiv_class_index1 is 0 or @is_dominated(equiv_class_index1)
      return false
    member = @ensure_member(equiv_class_index1)
    member.spogi = spogi
    member.value = spogi.o.getNativeValue()member.outer_aggr = @outer_aggr @value = member.value @spogi = spogi
    return member
  is_dominated: (equiv_class_index1) ->
    if not @test_for_domination
      return false
    by_class = equiv_class_index1 - 1
    while by_class > 0
      if @members[by_class]?
        return true
      by_class = by_class - 1
    return false
class GenericElector extends GenericAggregator
  promote_member: (spogi, rollup, member) ->
    @promote_spogi(spogi, rollup, member)
    @promote_value(spogi, rollup, member)
class GenericDigestor extends GenericAggregator
  promote_member: (spogi, rollup, member) ->
    @promote_value(spogi, rollup, member)
class GENERIC.Latest extends GenericElector
  should_promote: (spogi, rollup, member) ->
    return rollup.spogi.ww().cmp(member.spogi.ww()) < 0
GENERIC['↻'] = GENERIC.Latest
class GENERIC.Earliest extends GenericElector
  should_promote: (spogi, rollup, member) ->
    return rollup.spogi.ww().cmp(member.spogi.ww()) > 0
GENERIC['↺'] = GENERIC.Earliest
class GENERIC.Max extends GenericDigestor # TODO make this an Elector or document why not
  should_promote: (spogi, rollup, member) ->
    return rollup.value < member.value
GENERIC['↟'] = GENERIC.Max
class GENERIC.Min extends GenericDigestor # TODO make this an Elector or document why not
  should_promote: (spogi, rollup, member) ->debugger
    return rollup.value > member.value
GENERIC['↡'] = GENERIC.Min
class GENERIC.Every extends GenericElector
  should_promote: (spogi, rollup, member) ->
    return true
GENERIC['∀'] = GENERIC.Every
class GENERIC.Average extends GenericDigestor
  should_promote: (spogi, rollup, member) ->
    return true
  promote_value: (spogi, rollup, member) ->
    rollup.value_prior = rollup.value or 0
    if not rollup.count?
      rollup.count = 0
    count_prior = rollup.count
    rollup.count = rollup.count + 1
    rollup.value = (count_prior * rollup.value_prior + member.value) / rollup.countTODO get Distinct working class GenericAccumulator extends GenericAggregator @docs = “An Accumulator ‘collects’ the members into a Set”
class GENERIC.Distinct extends GenericAccumulator @docs = “Superset ‘collects’ all the distinct values” should_promote: (spogi, rollup, member) -> return true promote_value: (spogi, rollup, member) ->
WhoToHeed() use cases: me Just heed me me, personA Just heed me and personA me > personA Heed me over personA (ie my evaluations instead of theirs) (me, personA) > personB Heed me and personA over personB me > (personA, personB) Heed me over personA and personB me > (personA, personB) > everyone Heed me over personA and personB and them over everyone else me > everyone > (personA, personB) Heed me over everyone and everyone over personA and personB me > everyone Heed me over everyone everyone Heed everyone everyone - personA Heed everyone except personA everyone - (personA, personB) Heed everyone except personA and personB me > (tom, bob) > (everyone - (personA, personB)) > personC > personA - personB Problems: How to represent everyone? 1 How to represent exclusion? who_to_heed: include: true # everyone exclude: new Set([‘tom’]) chain: include: new Set([‘tom’]) exclude: new Set([‘bob’])
  # Notes:
  #   if chain then exclude membership does not mean reject, yet
How to represent named groups of people? (the admins, my friends, black hats) How to represent people playing roles? (owner, member, creator, admin)
EVERYONE = '_everyone'
OTHERS = '_others'
NOTME = '_notme'
ME = '_me'
class WhoToHeed
  @build_tiers_from_args: (args_ast, me_is) ->Build a tree structure with nodes like: node: include: [true] # or [alice, bob, ladygaga] exclude: [alice, bob, ladygaga] chain: # another node
    tiers =
      incl_all:{}
      excl_all:{}
    current_tier = tiers
    for arg in args_ast
      if not current_tier.include?
        current_tier.include = []
        incl_or_excl = current_tier.include
        incl_or_excl_all = tiers.incl_all
      if arg.type in ['atuser','genuser']
        if arg.type is 'atuser'
          val_to_push = arg.value
        else # arg.type is 'genuser' ie everyone|me|others|notme
          if arg.value is 'me'
            val_to_push = me_is
          else
            val_to_push = '_' + arg.value
        incl_or_excl.push(val_to_push)
        incl_or_excl_all[val_to_push] = true #current_tier
      if arg.type is 'punc'
        if arg.value is '~'
          if not current_tier.exclude?
            current_tier.exclude = []
          incl_or_excl = current_tier.exclude
          incl_or_excl_all = tiers.excl_all
        if arg.value is '|'
          current_tier.chain = {}
          current_tier = current_tier.chain
    return tiers
  constructor: (tier, me_or_top) ->Pass me_is as the second argument to the top of a WhoToHeed tree otherwise pass the top as the second argument.
    if typeof me_or_top is 'string'this IS the top of a WhoToHeed tree
      @me_is = me_or_top
      @top = @
      @tier = tier
    else
      @top = me_or_top
    if tier is 'everyone'
      tier = 1
    tier ?= 0 # default to heeding no one if nothing is said to the contrary
    if typeof tier is 'number'
      @force = tier
      return
    if tier.exclude?
      @exclude = new Set(tier.exclude)
    if tier.include?
      @include = new Set(tier.include)
    if tier.chain?
      @chain = new WhoToHeed(tier.chain, @.top)
  heed: (who, otherwise, equiv) ->Return either 0 or the 1-based index of the “Heeding equivalency class”
    if @force?
      return @force
    if equiv?
      equiv = equiv + 1
    else
      equiv = 1
    me_is = @top.me_is
    if not me_is?
      throw new Error("@top.me_is must be defined")
    if @exclude?
      if @exclude.has(true) # true means EVERYONE
        throw new Error("user refs should never be true, rather _everyone, _others, _notme")
      if @exclude.has(who)
        if @chain?
          return @chain.heed(who, 0, equiv)
        else
          return 0
      if (@exclude.has(OTHERS) and
          (not @top.tier.excl_all[me_is]) and (not @top.tier.incl_all[me_is]))
        return 0
    if @include?
      if @include.has(who) # explicit inclusion dominates chained exclusion
        return equiv
      if @include.has(true) # true means EVERYONE
        throw new Error("user refs should never be true, rather _everyone, _others, _notme or @WHATEVER!!!")
      if @include.has(EVERYONE)
        otherwise = equivif not @me_is? throw new Error(“@me_is must be defined”) else console.log “me_is:”, @me_is
      if who isnt me_is and @include.has(NOTME)
        return equiv
      if (@include.has(OTHERS) and
          (not @top.tier.excl_all[me_is]) and (not @top.tier.incl_all[me_is]))
        otherwise = equiv
    if @chain?
      return @chain.heed(who, otherwise, equiv)
    if otherwise?
      return otherwise
    return equiv
expect = (val) ->
  to:
    equal: (check, msg) ->
      if check isnt val
        console.log "expected #{val} to equal #{check}", msg or ""
        debugger
      else
        console.log "."
DataReduction_suite = (noodb) ->
  if not noodb?
    NooDB = require((not window? and '../lib/' or '') + 'noodb').NooDB
    Log = require("log")
    log = new Log('ERROR')
    ARGS = {verbosity: 0, log: log}
    noodb = new NooDB("test/test_noodb.nq", "http://example.com/", ARGS)
  window.IN_JIG = true
  jig = new ReductionJig(noodb, "Latest(everyone)", 'bob')
  redox = jig.reduction
  aggr_chains = redox.get_aggregators_for_user('ann')
  expect(jig.do({s:'_:gilligan', p:'_:iq', o:75, g:'_:minnow'})).to.equal(75)
  expect(jig.do({o:76})).to.equal(76)
  expect(jig.do({o:77})).to.equal(77)
  jig = new ReductionJig(noodb, "Min(Latest(everyone))", 'bob')
  expect(jig.do({s:'_:gilligan', p:'_:iq', o:75, g:'_:minnow'})).to.equal(75)
  expect(jig.do({o:76})).to.equal(75)
  expect(jig.do({o:74})).to.equal(74)
  jig = new ReductionJig(noodb, "Average(Latest(everyone))", 'bob')
  redox = jig.reduction
  heed_elector = redox.get_aggregators_for_user('ann')[0]
  expect(heed_elector.outer_aggr.constructor.name).to.equal('Latest')
  expect(heed_elector.outer_aggr.outer_aggr.constructor.name).to.equal('Average')
  expect(jig.do({s:'_:ginger', p:'_:iq', o:120, g:'_:minnow'})).to.equal(120)
  expect(jig.do({o:60})).to.equal((120+60)/2)        # 180/2 => 90
  expect(jig.do({o:180})).to.equal((120+60+180)/3)   # 360/3 => 120
  expect(jig.do({o:40})).to.equal((120+60+180+40)/4) # 400/4 => 100
  console.log "SUCCESS"
  expect(1).to.equal(2, 'PROOF THAT THIS GHETTO expect() WORKS')
Discriminator_suite = (noodb) ->
  console.clear()
  if not noodb?
    NooDB = require((not window? and '../lib/' or '') + 'noodb').NooDB
    Log = require("log")
    log = new Log('ERROR')
    ARGS = {verbosity: 0, log: log}
    noodb = new NooDB("test/test_noodb.nq", "http://example.com/", ARGS)
  alice = {user_symbol: 'alice', session_no: 1}
  bob = {user_symbol: 'bob', session_no: 1}
  carol = {user_symbol: 'carol', session_no: 1}
  dan = {user_symbol: 'dan', session_no: 1}
  eve = {user_symbol: 'eve', session_no: 1}
  fred = {user_symbol: 'fred', session_no: 1}
  window.IN_JIG = false
  if true
    jig = new DiscriminatorJig(noodb, "Every(Latest(Every(@alice,@bob|@carol,@dan)))", 'bob')
    initial_spogi = {s:'_:gilligan', p:'iq', o:75, g:'_:minnow', who:fred}
    xtnd = _.extend
    expect(jig.disc(initial_spogi)).to.equal(null)
    expect(jig.disc({o:120, who:dan})).to.equal(120)
    expect(jig.disc({o:130, who:bob})).to.equal(130)
    expect(jig.disc({o:120, who:carol})).to.equal(null, "should be dominated by bob")
    expect(jig.disc({o:140, who:alice})).to.equal(140, "alice should replace bob")
  if true
    jig = new DiscriminatorJig(noodb, "Min(Latest(me),Latest(others))", 'bob')
    initial_spogi = {s:'_:gilligan', p:'iq', o:75, g:'_:minnow', who:alice}
    xtnd = _.extend
    expect(jig.disc(initial_spogi)).to.equal(75, "alice's value")
    expect(jig.disc(xtnd(initial_spogi,{o:74, who:bob}))).to.equal(74,
      "bob's latest is lower")
    expect(jig.disc(xtnd(initial_spogi,{o:76, who:bob}))).to.equal(null,
      "bob's latest is more than alice's so show hers")
  if true
    jig = new ReductionJig(noodb, "Latest(notme)", 'bob')
    first_spogi = {s:'_:doc', p:'_:iq', o:140, g:'_:minnow', who:bob}
    expect(jig.do(first_spogi)).to.equal(null)
    expect(jig.do({o:120, who:carol})).to.equal(120)
    expect(jig.do({o:145, who:bob})).to.equal(120)
    expect(jig.do({o:135, who:alice})).to.equal(135)
  if true
    jig = new ReductionJig(noodb, "Latest(everyone~@alice)", 'bob')
    first_spogi = {s:'_:doc', p:'_:iq', o:140, g:'_:minnow', who:bob}
    expect(jig.do(first_spogi)).to.equal(140)
    expect(jig.do({o:120, who:carol})).to.equal(120)
    expect(jig.do({o:145, who:dan})).to.equal(145)
    expect(jig.do({o:135, who:alice})).to.equal(145)
  if true
    jig = new DiscriminatorJig(noodb, "Latest(everyone).re(iq)", 'bob')
    expect(jig.disc({s:'_:gilligan', p:'iq', o:75, g:'_:minnow'})).to.equal(75)
    expect(jig.disc({o:76})).to.equal(76)
    expect(jig.disc({o:77})).to.equal(77)
    expect(jig.disc({p:'lb', o:98})).to.equal(null)
  if true
    jig = new DiscriminatorJig(noodb, "Average(Latest(everyone))", 'bob')
    gilligan_iq = {s:'_:gilligan', p:'iq', o:120, g:'_:minnow'}
    skipper_iq = {s:'_:skipper', p:'iq', o:150, g:'_:minnow'}
    xtnd = _.extendOwn
    expect(jig.disc(gilligan_iq)).to.equal(120)
    expect(jig.disc(skipper_iq)).to.equal(150)
    expect(jig.disc(xtnd(gilligan_iq,{o:60}))).to.equal((120+60)/2) # 90
    expect(jig.disc(xtnd(skipper_iq,{o:50}))).to.equal((150+50)/2) # 100
    expect(jig.disc(xtnd(gilligan_iq,{o:180}))).to.equal((120+60+180)/3) # 120
    expect(jig.disc(xtnd(skipper_iq,{o:130}))).to.equal((150+50+130)/3) # 110
    expect(jig.disc(xtnd(gilligan_iq,{o:40}))).to.equal((120+60+180+40)/4) # 100
    expect(jig.disc(xtnd(skipper_iq,{o:82}))).to.equal((150+50+130+82)/4) # 103 = 412/
  console.log "SUCCESS"
  expect(1).to.equal(2, 'PROOF THAT THIS GHETTO expect() WORKS')
WhoToHeed_suite = () -> # same as in test/test_noodb.coffee but here for debugging
  no_one = new WhoToHeed()
  expect(no_one.heed('bob')).to.equal(0)
  everyone = new WhoToHeed
    include: [true]
  expect(everyone.heed('bob')).to.equal(1)
  force_everyone = new WhoToHeed(1)
  expect(force_everyone.heed('bob')).to.equal(1)
  force_nobody = new WhoToHeed(0)
  expect(force_nobody.heed('bob')).to.equal(0)
  not_bob = new WhoToHeed
    include: [true]
    exclude: ['bob']
  expect(not_bob.heed('alice')).to.equal(1)
  expect(not_bob.heed('bob')).to.equal(0)
  not_bert_and_annie_last = new WhoToHeed
    include: [true]
    exclude: ['bert','annie']
    chain:
      include: ['annie']
  expect(not_bert_and_annie_last.heed('bert')).to.equal(0, "looking for bert")
  expect(not_bert_and_annie_last.heed('annie')).to.equal(2, "looking for annie")
  everyone_but_bob = new WhoToHeed
    include: [true]
    chain:
      exclude: ['bob']
  expect(everyone_but_bob.heed('bob')).to.equal(0)
  expect(everyone_but_bob.heed('alice')).to.equal(1)
  msg = "This intentionally broken test shows that expect() works"
  expect(everyone_but_bob.heed('alice')).to.equal(false, msg)WhoToHeed_suite()
class Discriminator
  @docs = "A Discriminator spawns a PredicateReducer per unique Subject-Criterion pairing"
  constructor: (discriminator_src, @me_is) ->
    @criterion_reducer_mapping = {}
    @default_reducer = null
    if not DiscriminatorReader?
      discr_pth = (not window? and '../lib/' or '') + 'discriminator'
      DiscriminatorReader = require(discr_pth).DiscriminatorReader
    @discr_ast = new DiscriminatorReader(discriminator_src)
    @populate_criterion_reducer_mapping_from_discr_ast()
    @s_p_to_reduction = {}
  populate_criterion_reducer_mapping_from_discr_ast: () ->
    @tree = {}
    if not (@discr_ast? and @discr_ast.ast and @discr_ast.ast.prog)
      throw new Error("no Discriminator was parsed")
    predicate_reducer_ASTs = @discr_ast.ast.prog
    last_data_reducer = predicate_reducer_ASTs[predicate_reducer_ASTs.length - 1]
    for predicate_reducer_AST in predicate_reducer_ASTs
      new_pred_red = new DataReducer(predicate_reducer_AST, @me_is)
      for p_key in new_pred_red.re_predicates
        old_pred_red = @criterion_reducer_mapping[p_key]
        @criterion_reducer_mapping[p_key] = new_pred_red
      if new_pred_red.re_predicates.length is 0
        @default_reducer = new_pred_red
  discriminate: (spogi, noodb) ->returns this spogi OR a synthetic one (if the product of digestion) OR null
    p_key = spogi.p.key()
    reduction = @get_reduction_for_spogi(spogi)
    if reduction? and reduction.consume_spogi(spogi)if it returns null the spogi did not affect the summary
      return @get_real_or_synthetic(reduction, spogi, noodb)
    return null
  get_reduction_for_spogi: (spogi) ->The goal of this method is to return a reduction tuned to the subject, the predicate and the current user (see @me_is)
There are many different ways a user (say @bob) can be processed:
    s_key = spogi.s.key()
    p_key = spogi.p.key()
    s_p_key = s_key + " " + p_key
    reduction = @s_p_to_reduction[s_p_key]
    if not reduction
      reducer = @get_reducer_for_p(p_key)
      reduction = reducer? and reducer.spawn(s_p_key) or null
      @s_p_to_reduction[s_p_key] = reduction
    return reduction
  get_reducer_for_p: (p_key) ->
    retval = @criterion_reducer_mapping[p_key]
    if not retval?console.log “using @default_reducer for #{p_key}”, @default_reducer
      return @default_reducer
    return retval or @default_reducer
  get_real_or_synthetic: (reduction, spogi, noodb) ->Return Elected spogis or synthesize a spogi
    ret_spogi = reduction.spogi # only produced by GenericElector subclasses
    if not ret_spogi?  # so if no ret_spogi let us synthesize one
      sess =TODO Examine how to make this a reference to the Discriminator on the theory that the Discriminator is the “Agent” responsible for creating the synthetic value.
        user_symbol: 'synthetic'
        session_no: 1  # TODO examine this for improvement
      s_key = spogi.s.key()
      p_key = spogi.p.key()
      g_key = spogi.g.key()
      if window?
        ret_spogi = noodb.allege_local(s_key, p_key, reduction.value, g_key, sess)
      else
        ret_spogi = noodb.allege(s_key, p_key, reduction.value, g_key, sess)
    return ret_spogi
class ReductionJig
  doc: ".do() returns the current value of a DataReduction starting with null.  It is assumed that all spogi changes sent to .do() are for the same S_P pair."
  constructor: (@noodb, @discr_form, @me_is) ->
    @reducer = new DataReducer(discr_form, @me_is)
    @reduction = @reducer.spawn()
  do: (new_args) ->
    @args ?= {}
    for k,v of new_args
      @args[k] = v
    a = @args
    if window? # is this the browser environment? (as opposed to ther server)
      a_spogi = @noodb.allege_local(a.s, a.p, a.o, a.g, a.who)
    else
      a_spogi = @noodb.allege(a.s, a.p, a.o, a.g, a.who)
    if not @reduction.consume_spogi
      console.log "@reduction", @reduction
      debugger
    @reduction.consume_spogi(a_spogi)
    return @reduction.value
class DiscriminatorJig
  doc: ".disc() runs .discriminate() returning a spogi IF THERE WAS CHANGE else null"
  constructor: (@noodb, @discr_form, @me_is) ->
    @discriminator = new Discriminator(discr_form, @me_is)
  disc: (new_args) ->
    @args ?= {}
    for k,v of new_args
      @args[k] = v
    a = @args
    if window?
      a_spogi = @noodb.allege_local(a.s, a.p, a.o, a.g, a.who)
    else
      a_spogi = @noodb.allege(a.s, a.p, a.o, a.g, a.who)
    onward = @discriminator.discriminate(a_spogi, @noodb)
    if onward
      return onward.o.getNativeValue()
    return null
(exports ? this).WhoToHeed = WhoToHeed(exports ? this).WhoToHeed_suite = WhoToHeed_suite
(exports ? this).DataReduction = DataReduction
(exports ? this).DataReduction_suite = DataReduction_suite
(exports ? this).DataReducer = DataReducer
(exports ? this).Discriminator = Discriminator
(exports ? this).ReductionJig = ReductionJig
(exports ? this).DiscriminatorJig = DiscriminatorJig
(exports ? this).Discriminator_suite = Discriminator_suite