Module: Fbe

Defined in:
lib/fbe.rb

Overview

The main and only module of this gem.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2024-2025 Zerocracy

License

MIT

Defined Under Namespace

Modules: Middleware Classes: Award, Conclude, FakeOctokit, Graph, Iterate

Constant Summary collapse

VERSION =

Current version of the gem (changed by .rultor.yml on every release)

'0.26.3'

Class Method Summary collapse

Class Method Details

.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>

Generates policies/bylaws from Liquid templates.

Using the templates stored in the assets/bylaws directory, this function creates a hash where keys are bylaw names (derived from filenames) and values are the rendered formulas. Templates can use three parameters to control the strictness and generosity of the bylaws.

Examples:

Generate balanced bylaws

bylaws = Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
bylaws['bug-report-was-rewarded']
# => "award { 2 * love * paranoia }"

Generate strict bylaws with minimal rewards

bylaws = Fbe.bylaws(anger: 4, love: 1, paranoia: 3)
bylaws['dud-was-punished']
# => "award { -16 * anger }"

Parameters:

  • anger (Integer) (defaults to: 2)

    Strictness level for punishments (0-4, default: 2)

    • 0: Very lenient, minimal punishments

    • 2: Balanced approach (default)

    • 4: Very strict, maximum punishments

  • love (Integer) (defaults to: 2)

    Generosity level for rewards (0-4, default: 2)

    • 0: Minimal rewards

    • 2: Balanced rewards (default)

    • 4: Maximum rewards

  • paranoia (Integer) (defaults to: 2)

    Requirements threshold for rewards (1-4, default: 2)

    • 1: Easy to earn rewards

    • 2: Balanced requirements (default)

    • 4: Very difficult to earn rewards

Returns:

  • (Hash<String, String>)

    Hash mapping bylaw names to their formulas

Raises:

  • (RuntimeError)

    If parameters are out of valid ranges



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/fbe/bylaws.rb', line 38

def Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
  raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5
  raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5
  raise "The 'paranoia' must be in the [1..4] interval: #{paranoia.inspect}" unless paranoia.positive? && paranoia < 5
  home = File.join(__dir__, '../../assets/bylaws')
  raise "The directory with templates is absent #{home.inspect}" unless File.exist?(home)
  Dir[File.join(home, '*.liquid')].to_h do |f|
    formula = Liquid::Template.parse(File.read(f)).render(
      'anger' => anger, 'love' => love, 'paranoia' => paranoia
    )
    [File.basename(f).gsub(/\.liquid$/, ''), formula]
  end
end

.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, time: Time) {|Factbase::Fact| ... } ⇒ Object

Creates an instance of Conclude and evals it with the block provided.

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Yields:

  • (Factbase::Fact)

    The fact



20
21
22
23
24
25
26
27
28
# File 'lib/fbe/conclude.rb', line 20

def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, time: Time, &)
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:, time:)
  c.instance_eval(&)
end

.copy(source, target, except: []) ⇒ Integer

Note:

Existing properties in target are preserved (not overwritten)

Makes a copy of a fact, moving all properties to a new fact.

All properties from the source will be copied to the target, except those listed in the except array. Only copies properties that don’t already exist in the target. Multi-valued properties are copied with all their values.

Examples:

Copy all properties except timestamps

source = fb.query('(eq type "user")').first
target = fb.insert
count = Fbe.copy(source, target, except: ['_time', '_id'])
puts "Copied #{count} property values"

Parameters:

  • source (Factbase::Fact)

    The source fact to copy from

  • target (Factbase::Fact)

    The target fact to copy to

  • except (Array<String>) (defaults to: [])

    List of property names to NOT copy (defaults to empty)

Returns:

  • (Integer)

    The number of property values that were copied

Raises:

  • (RuntimeError)

    If source, target, or except is nil



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/fbe/copy.rb', line 26

def Fbe.copy(source, target, except: [])
  raise 'The source is nil' if source.nil?
  raise 'The target is nil' if target.nil?
  raise 'The except is nil' if except.nil?
  copied = 0
  source.all_properties.each do |k|
    next unless target[k].nil?
    next if except.include?(k)
    source[k].each do |v|
      target.send(:"#{k}=", v)
      copied += 1
    end
  end
  copied
end

.delete(fact, *props, fb: Fbe.fb, id: '_id') ⇒ Factbase::Fact

Delete properties from a fact by creating a new fact without them.

This method doesn’t modify the original fact. Instead, it deletes the existing fact from the factbase and creates a new one with all properties except those specified for deletion.

Examples:

Delete multiple properties from a fact

fact = fb.query('(eq type "user")').first
new_fact = Fbe.delete(fact, 'age', 'city')
# new_fact will have all properties except 'age' and 'city'

Parameters:

  • fact (Factbase::Fact)

    The fact to delete properties from (must have an ID)

  • props (Array<String>)

    List of property names to delete

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)

  • id (String) (defaults to: '_id')

    The property name used as unique identifier (defaults to ‘_id’)

Returns:

  • (Factbase::Fact)

    New fact without the deleted properties

Raises:

  • (RuntimeError)

    If fact is nil, has no ID, or ID property doesn’t exist



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/fbe/delete.rb', line 25

def Fbe.delete(fact, *props, fb: Fbe.fb, id: '_id')
  raise 'The fact is nil' if fact.nil?
  i = fact[id]
  raise "There is no #{id.inspect} in the fact" if i.nil?
  i = i.first
  before = {}
  fact.all_properties.each do |k|
    next if props.include?(k)
    fact[k].each do |v|
      before[k] = v
    end
  end
  fb.query("(eq #{id} #{i})").delete!
  c = fb.insert
  before.each do |k, v|
    c.send(:"#{k}=", v)
  end
  c
end

.enter(badge, why, options: $options, loog: $loog) { ... } ⇒ Object

Note:

Requires $options and $loog global variables to be set

Note:

In testing mode (options.testing != nil), bypasses valve recording

Enter a new valve in the Zerocracy system.

A valve is a checkpoint or gate in the processing pipeline. This method records the entry into a valve with a reason, unless in testing mode.

Examples:

Enter a valve for processing

Fbe.enter('payment-check', 'Validating payment data') do
  # Process payment validation
  validate_payment(data)
end

Parameters:

  • badge (String)

    Unique badge identifier for the valve

  • why (String)

    The reason for entering this valve

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options if not provided)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog if not provided)

Yields:

  • Block to execute within the valve context

Returns:

  • (Object)

    The result of the yielded block

Raises:

  • (RuntimeError)

    If badge, why, or required globals are nil



28
29
30
31
32
33
34
35
36
# File 'lib/fbe/enter.rb', line 28

def Fbe.enter(badge, why, options: $options, loog: $loog, &)
  raise 'The badge is nil' if badge.nil?
  raise 'The why is nil' if why.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  return yield unless options.testing.nil?
  baza = BazaRb.new('api.zerocracy.com', 443, options.zerocracy_token, loog:)
  baza.enter(options.job_name, badge, why, options.job_id.to_i, &)
end

.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase

Returns an instance of Factbase (cached).

Instead of using $fb directly, it is recommended to use this utility method. It will not only return the global factbase, but will also make sure it’s properly decorated and cached.

Parameters:

  • fb (Factbase) (defaults to: $fb)

    The global factbase provided by the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Returns:

  • (Factbase)

    The global factbase



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/fbe/fb.rb', line 29

def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog)
  raise 'The fb is nil' if fb.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  global[:fb] ||=
    begin
      rules = Dir.glob(File.join('rules', '*.fe')).map { |f| File.read(f) }
      fbe = Factbase::Rules.new(
        fb,
        "(and \n#{rules.join("\n")}\n)",
        uid: '_id'
      )
      fbe =
        Factbase::Pre.new(fbe) do |f, fbt|
          max = fbt.query('(max _id)').one
          f._id = (max.nil? ? 0 : max) + 1
          f._time = Time.now
          f._version = "#{Factbase::VERSION}/#{Judges::VERSION}/#{options.action_version}"
          f._job = options.job_id unless options.job_id.nil?
        end
      Factbase::Impatient.new(
        Factbase::Logged.new(
          Factbase::SyncFactbase.new(
            Factbase::IndexedFactbase.new(
              Factbase::CachedFactbase.new(
                fbe
              )
            )
          ),
          loog
        ),
        timeout: 60
      )
    end
end

.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph

Creates an instance of Graph.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility

Returns:



17
18
19
20
21
22
23
24
25
# File 'lib/fbe/github_graph.rb', line 17

def Fbe.github_graph(options: $options, global: $global, loog: $loog)
  global[:github_graph] ||=
    if options.testing.nil?
      Fbe::Graph.new(token: options.github_token || ENV.fetch('GITHUB_TOKEN', nil))
    else
      loog.debug('The connection to GitHub GraphQL API is mocked')
      Fbe::Graph::Fake.new
    end
end

.if_absent(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ nil, Factbase::Fact

Note:

String values are properly escaped in queries

Note:

Time values are converted to UTC ISO8601 format for comparison

Injects a fact if it’s absent in the factbase, otherwise returns nil.

Checks if a fact with the same property values already exists. If not, creates a new fact. System properties (_id, _time, _version) are excluded from the uniqueness check.

Here is what you do when you want to add a fact to the factbase, but don’t want to make a duplicate of an existing one:

require 'fbe/if_absent'
n = Fbe.if_absent do |f|
  f.what = 'something'
  f.details = 'important'
end
return if n.nil?  # Fact already existed
n.when = Time.now # Add additional properties to the new fact

This code will definitely create one fact with what equals to something and details equals to important, while the when will be equal to the time of its first creation.

Examples:

Ensure unique user registration

user = Fbe.if_absent do |f|
  f.type = 'user'
  f.email = '[email protected]'
end
if user
  user.registered_at = Time.now
  puts "New user created"
else
  puts "User already exists"
end

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to check and insert into (defaults to Fbe.fb)

Yields:

  • (Factbase::Fact)

    A proxy fact object to set properties on

Returns:

  • (nil, Factbase::Fact)

    nil if fact exists, otherwise the newly created fact



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/fbe/if_absent.rb', line 48

def Fbe.if_absent(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        k = k[0..-2].to_sym
        v = args[1]
        raise "Can't set #{k} to nil" if v.nil?
        raise "Can't set #{k} to empty string" if v.is_a?(String) && v.empty?
        @map[k] = v
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.to_a.first
  return nil if before
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
end

.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String

Note:

Requires ‘repository’ and ‘issue’ properties in the fact

Note:

Repository names are cached to reduce GitHub API calls

Converts GitHub repository and issue IDs into a formatted issue reference.

Takes the repository and issue properties from the provided fact, queries the GitHub API to get the repository’s full name, and formats it as a standard GitHub issue reference (e.g., “zerocracy/fbe#42”). Results are cached globally to minimize API calls.

Examples:

Format an issue reference

issue_fact = fb.query('(eq type "issue")').first
issue_fact.repository = 549866411  # Repository ID
issue_fact.issue = 42               # Issue number
puts Fbe.issue(issue_fact)  # => "zerocracy/fbe#42"

Parameters:

  • fact (Factbase::Fact)

    The fact containing repository and issue properties

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Returns:

  • (String)

    Formatted issue reference (e.g., “owner/repo#123”)

Raises:

  • (RuntimeError)

    If fact is nil or required properties are missing

  • (RuntimeError)

    If required global variables are not set



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/fbe/issue.rb', line 30

def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
  raise 'The fact is nil' if fact.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  rid = fact['repository']
  raise "There is no 'repository' property" if rid.nil?
  rid = rid.first.to_i
  issue = fact['issue']
  raise "There is no 'issue' property" if issue.nil?
  issue = issue.first.to_i
  "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(rid)}##{issue}"
end

.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) { ... } ⇒ Object

Creates an instance of Iterate and evaluates it with the provided block.

This is a convenience method that creates an iterator instance and evaluates the DSL block within its context. The iterator processes repositories defined in options.repositories, executing queries and managing state for each.

Examples:

Iterate through repositories processing issues

Fbe.iterate do
  as 'issues-iterator'
  by '(and (eq what "issue") (gt created_at $before))'
  repeats 5
  quota_aware
  over(timeout: 300) do |repository_id, issue_id|
    process_issue(repository_id, issue_id)
    issue_id + 1
  end
end

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The global factbase provided by the judges tool (defaults to Fbe.fb)

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • Block containing DSL methods (as, by, over, etc.) to configure iteration

Returns:

  • (Object)

    Result of the block evaluation

Raises:

  • (RuntimeError)

    If required globals are not set



38
39
40
41
42
43
44
45
# File 'lib/fbe/iterate.rb', line 38

def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
  raise 'The fb is nil' if fb.nil?
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  c = Fbe::Iterate.new(fb:, loog:, options:, global:)
  c.instance_eval(&)
end

.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact

Note:

System attributes (_id, _time, _version) are ignored when matching

Ensures exactly one fact exists with the specified attributes in the factbase.

This method creates a new fact if none exists with the given attributes, or returns an existing fact if one already matches. Useful for preventing duplicate facts while ensuring required facts exist.

Examples:

Creating or finding a unique fact

require 'fbe/just_one'
fact = Fbe.just_one do |f|
  f.what = 'github_issue'
  f.issue_id = 123
  f.repository = 'zerocracy/fbe'
end
# Returns existing fact if one exists with these exact attributes,
# otherwise creates and returns a new fact

Attributes are matched exactly (case-sensitive)

Fbe.just_one { |f| f.name = 'Test' }  # Creates fact with name='Test'
Fbe.just_one { |f| f.name = 'test' }  # Creates another fact (different case)

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to search/insert into (defaults to Fbe.fb)

Yields:

  • (Factbase::Fact)

    Block to set attributes on the fact

Returns:

  • (Factbase::Fact)

    The existing or newly created fact



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/fbe/just_one.rb', line 35

def Fbe.just_one(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        @map[k[0..-2].to_sym] = args[1]
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.to_a.first
  return before unless before.nil?
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
end

.mask_to_regex(mask) ⇒ Regexp

Converts a repository mask pattern to a regular expression.

Examples:

Basic wildcard matching

Fbe.mask_to_regex('zerocracy/*')
# => /zerocracy\/.*/i

Specific repository (no wildcard)

Fbe.mask_to_regex('zerocracy/fbe')
# => /zerocracy\/fbe/i

Parameters:

  • mask (String)

    Repository mask in format ‘org/repo’ where repo can contain ‘*’

Returns:

  • (Regexp)

    Case-insensitive regular expression for matching repositories

Raises:

  • (RuntimeError)

    If organization part contains asterisk



23
24
25
26
27
# File 'lib/fbe/unmask_repos.rb', line 23

def Fbe.mask_to_regex(mask)
  org, repo = mask.split('/')
  raise "Org '#{org}' can't have an asterisk" if org.include?('*')
  Regexp.compile("#{org}/#{repo.gsub('*', '.*')}", Regexp::IGNORECASE)
end

.octo(options: $options, global: $global, loog: $loog) ⇒ Hash

Makes a call to the GitHub API.

It is supposed to be used instead of Octokit::Client, because it is pre-configured and enables additional features, such as retrying, logging, and caching.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility

Options Hash (options:):

  • :github_token (String)

    GitHub API token for authentication

  • :testing (Boolean)

    When true, uses FakeOctokit for testing

  • :sqlite_cache (String)

    Path to SQLite cache file for HTTP responses

  • :sqlite_cache_maxsize (Integer)

    Maximum size of SQLite cache in bytes (default: 10MB)

Returns:

  • (Hash)

    Usually returns a JSON, as it comes from the GitHub API



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/fbe/octo.rb', line 41

def Fbe.octo(options: $options, global: $global, loog: $loog)
  raise 'The $global is not set' if global.nil?
  raise 'The $options is not set' if options.nil?
  raise 'The $loog is not set' if loog.nil?
  global[:octo] ||=
    begin
      trace = []
      if options.testing.nil?
        o = Octokit::Client.new
        token = options.github_token
        if token.nil?
          loog.debug("The 'github_token' option is not provided")
          token = ENV.fetch('GITHUB_TOKEN', nil)
          if token.nil?
            loog.debug("The 'GITHUB_TOKEN' environment variable is not set")
          else
            loog.debug("The 'GITHUB_TOKEN' environment was provided")
          end
        else
          loog.debug("The 'github_token' option was provided (#{token.length} chars)")
        end
        if token.nil?
          loog.warn('Accessing GitHub API without a token!')
        elsif token.empty?
          loog.warn('The GitHub API token is an empty string, won\'t use it')
        else
          o = Octokit::Client.new(access_token: token)
        end
        o.auto_paginate = true
        o.per_page = 100
        o.connection_options = {
          request: {
            open_timeout: 15,
            timeout: 15
          }
        }
        stack =
          Faraday::RackBuilder.new do |builder|
            builder.use(
              Faraday::Retry::Middleware,
              exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [
                Octokit::TooManyRequests, Octokit::ServiceUnavailable
              ],
              max: 4,
              interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4,
              methods: [:get],
              backoff_factor: 2
            )
            builder.use(Octokit::Response::RaiseError)
            builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter)
            builder.use(Fbe::Middleware::RateLimit)
            builder.use(Fbe::Middleware::Trace, trace, ignores: [:fresh])
            if options.sqlite_cache
              maxsize = Filesize.from(options.sqlite_cache_maxsize || '100M').to_i
              maxvsize = Filesize.from(options.sqlite_cache_maxvsize || '100K').to_i
              cache_min_age = options.sqlite_cache_min_age&.to_i
              store = Fbe::Middleware::SqliteStore.new(
                options.sqlite_cache, Fbe::VERSION, loog:, maxsize:, maxvsize:, ttl: 24, cache_min_age:
              )
              loog.info(
                "Using HTTP cache in SQLite file: #{store.path} (" \
                "#{File.exist?(store.path) ? Filesize.from(File.size(store.path).to_s).pretty : 'file is absent'}, " \
                "max size: #{Filesize.from(maxsize.to_s).pretty}, max vsize: #{Filesize.from(maxvsize.to_s).pretty})"
              )
              builder.use(
                Faraday::HttpCache,
                store:, serializer: JSON, shared_cache: false, logger: Loog::NULL
              )
            else
              loog.info("No HTTP cache in SQLite file, because 'sqlite_cache' option is not provided")
              builder.use(
                Faraday::HttpCache,
                serializer: Marshal, shared_cache: false, logger: Loog::NULL
              )
            end
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
        unless token.nil? || token.empty?
          loog.info(
            "Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect}, " \
            "#{o.rate_limit.remaining} quota remaining)"
          )
        end
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      o =
        decoor(o, loog:, trace:) do
          def print_trace!(all: false, max: 5)
            if @trace.empty?
              @loog.debug('GitHub API trace is empty')
            else
              grouped =
                @trace.select { |e| e[:duration] > 0.05 || all }.group_by do |entry|
                  uri = URI.parse(entry[:url])
                  query = uri.query
                  query = "?#{query.ellipsized(40)}" if query
                  "#{uri.scheme}://#{uri.host}#{uri.path}#{query}"
                end
              message = grouped
                .sort_by { |_path, entries| -entries.count }
                .map do |path, entries|
                  [
                    '  ',
                    path.gsub(%r{^https://api.github.com/}, '/'),
                    ': ',
                    entries.count,
                    " (#{entries.sum { |e| e[:duration] }.seconds})"
                  ].join
                end
                .take(max)
                .join("\n")
              @loog.info(
                "GitHub API trace (#{grouped.count} URLs vs #{@trace.count} requests, " \
                "#{@origin.rate_limit!.remaining} quota left):\n#{message}"
              )
              @trace.clear
            end
          end

          def off_quota?(threshold: 50)
            left = @origin.rate_limit!.remaining
            if left < threshold
              @loog.info("Too much GitHub API quota consumed already (#{left} < #{threshold}), stopping")
              true
            else
              @loog.debug("Still #{left} GitHub API quota left (>#{threshold})")
              false
            end
          end

          def user_name_by_id(id)
            raise 'The ID of the user is nil' if id.nil?
            raise 'The ID of the user must be an Integer' unless id.is_a?(Integer)
            json = @origin.user(id)
            name = json[:login].downcase
            @loog.debug("GitHub user ##{id} has a name: @#{name}")
            name
          end

          def repo_id_by_name(name)
            raise 'The name of the repo is nil' if name.nil?
            json = @origin.repository(name)
            id = json[:id]
            raise "Repository #{name} not found" if id.nil?
            @loog.debug("GitHub repository #{name.inspect} has an ID: ##{id}")
            id
          end

          def repo_name_by_id(id)
            raise 'The ID of the repo is nil' if id.nil?
            raise 'The ID of the repo must be an Integer' unless id.is_a?(Integer)
            json = @origin.repository(id)
            name = json[:full_name].downcase
            @loog.debug("GitHub repository ##{id} has a name: #{name}")
            name
          end
        end
      o =
        intercepted(o) do |e, m, _args, _r|
          if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota?
            raise "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
          end
        end
      o
    end
end

.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact

Note:

This operation preserves all other properties during recreation

Note:

If property already has the same single value, no changes are made

Overwrites a property in the fact by recreating the entire fact.

If the property doesn’t exist in the fact, it will be added. If it does exist, the entire fact will be destroyed, a new fact created with all existing properties, and the specified property set with the new value.

It is important that the fact has the _id property. If it doesn’t, an exception will be raised.

Examples:

Update a user’s status

user = fb.query('(eq login "john")').first
updated_user = Fbe.overwrite(user, 'status', 'active')
# All properties preserved, only 'status' is set to 'active'

Parameters:

  • fact (Factbase::Fact)

    The fact to modify (must have _id property)

  • property (String)

    The name of the property to set

  • value (Any)

    The value to set (can be any type)

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase to use (defaults to Fbe.fb)

Returns:

  • (Factbase::Fact)

    Returns new fact if recreated, or original if unchanged

Raises:

  • (RuntimeError)

    If fact is nil, has no _id, or property is not a String



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/fbe/overwrite.rb', line 30

def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
  raise 'The fact is nil' if fact.nil?
  raise 'The fb is nil' if fb.nil?
  raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String)
  return fact if !fact[property].nil? && fact[property].size == 1 && fact[property].first == value
  before = {}
  fact.all_properties.each do |prop|
    before[prop.to_s] = fact[prop]
  end
  id = fact['_id']&.first
  raise 'There is no _id in the fact, cannot use Fbe.overwrite' if id.nil?
  raise "No facts by _id = #{id}" if fb.query("(eq _id #{id})").delete!.zero?
  n = fb.insert
  before[property.to_s] = [value]
  before.each do |k, vv|
    next unless n[k].nil?
    vv.each do |v|
      n.send(:"#{k}=", v)
    end
  end
  n
end

.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object

Takes configuration parameter from the “PMP” fact.

The factbase may have a few facts with the what set to pmp (stands for the “project management plan”). These facts contain information that configures the project. It is expected that every fact with the what set to pmp also contains the area property, which is set to one of nine values: scope, time, cost, etc. (the nine process areas in the PMBOK).

If a proper pmp fact is not found or the property is absent in the fact, this method throws an exception. The factbase must contain PMP-related facts. Most probably, a special judge must fill it up with such a fact.

The method uses a double nested ‘others` block to create a chainable interface that allows accessing configuration like:

Fbe.pmp.hr.reward_points
Fbe.pmp.cost.hourly_rate
Fbe.pmp.time.deadline

Examples:

# Get HR reward points from PMP configuration
points = Fbe.pmp.hr.reward_points

# Get hourly rate from cost area
rate = Fbe.pmp.cost.hourly_rate

# Get deadline from time area
deadline = Fbe.pmp.time.deadline

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Returns:

  • (Object)

    A proxy object that allows method chaining to access PMP properties



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/fbe/pmp.rb', line 43

def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog)
  others do |*args1|
    area = args1.first
    unless %w[cost scope hr time procurement risk integration quality communication].include?(area.to_s)
      raise "Invalid area #{area.inspect} (not part of PMBOK)"
    end
    others do |*args2|
      param = args2.first
      f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.to_a.first
      raise "Unknown area #{area.inspect}" if f.nil?
      r = f[param]
      raise "Unknown property #{param.inspect} in the #{area.inspect} area" if r.nil?
      r.first
    end
  end
end

.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil

Note:

Skips execution if judge was run within the interval period

Note:

The ‘since’ property is added to the fact when p_since_days is provided

Run the block provided every X days based on PMP configuration.

Executes a block periodically based on PMP (Project Management Plan) settings. The block will only run if it hasn’t been executed within the specified interval. Creates a fact recording when the judge was last run.

Examples:

Run a cleanup task every 3 days

Fbe.regularly('cleanup', 'days_between_cleanups', 'cleanup_history_days') do |f|
  f.total_cleaned = cleanup_old_records
  # PMP might have: days_between_cleanups=3, cleanup_history_days=30
end

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_days (String)

    PMP property name for interval (defaults to 7 days if not in PMP)

  • p_since_days (String) (defaults to: nil)

    PMP property name for since period (defaults to 28 days if not in PMP)

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase (defaults to Fbe.fb)

  • judge (String) (defaults to: $judge)

    The name of the judge (uses $judge global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • (Factbase::Fact)

    Fact to populate with judge execution details

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fbe/regularly.rb', line 31

def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  raise 'The area is nil' if area.nil?
  raise 'The p_every_days is nil' if p_every_days.nil?
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $loog is not set' if loog.nil?
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_days}))").each.to_a.first
  interval = pmp.nil? ? 7 : pmp[p_every_days].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} statistics have recently been collected, skipping now")
    return
  end
  f = fb.insert
  f.what = judge
  f.when = Time.now
  unless p_since_days.nil?
    days = pmp.nil? ? 28 : pmp[p_since_days].first
    since = Time.now - (days * 24 * 60 * 60)
    f.since = since
  end
  yield f
  nil
end

.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|Factbase::Fact| ... } ⇒ nil

Note:

Skips execution if judge was run within the interval period

Note:

Overwrites the ‘when’ property of existing judge fact

Run the block provided every X hours based on PMP configuration.

Similar to Fbe.regularly but works with hour intervals instead of days. Executes a block periodically, maintaining a single fact that tracks the last execution time. The fact is overwritten on each run rather than creating new facts.

Examples:

Run a monitoring task every 6 hours

Fbe.repeatedly('monitoring', 'hours_between_checks') do |f|
  f.servers_checked = check_all_servers
  f.issues_found = count_issues
  # PMP might have: hours_between_checks=6
end

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_hours (String)

    PMP property name for interval (defaults to 24 hours if not in PMP)

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase (defaults to Fbe.fb)

  • judge (String) (defaults to: $judge)

    The name of the judge (uses $judge global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Yields:

  • (Factbase::Fact)

    The judge fact to populate with execution details

Returns:

  • (nil)

    Nothing

Raises:

  • (RuntimeError)

    If required parameters or globals are nil



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fbe/repeatedly.rb', line 33

def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  raise 'The area is nil' if area.nil?
  raise 'The p_every_hours is nil' if p_every_hours.nil?
  raise 'The fb is nil' if fb.nil?
  raise 'The $judge is not set' if judge.nil?
  raise 'The $loog is not set' if loog.nil?
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_hours}))").each.to_a.first
  hours = pmp.nil? ? 24 : pmp[p_every_hours].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} has recently been executed, skipping now")
    return
  end
  f = fb.query("(and (eq what '#{judge}'))").each.to_a.first
  if f.nil?
    f = fb.insert
    f.what = judge
  end
  Fbe.overwrite(f, 'when', Time.now)
  yield fb.query("(and (eq what '#{judge}'))").each.to_a.first
  nil
end

.sec(fact, prop = :seconds) ⇒ String

Note:

Uses the tago gem’s ago method for formatting

Converts number of seconds into human-readable time format.

The number of seconds is taken from the fact provided, usually stored there in the seconds property. The seconds are formatted into a human-readable string like “3 days ago” or “5 hours ago” using the tago gem.

Examples:

Format elapsed time from a fact

build_fact = fb.query('(eq type "build")').first
build_fact.duration = 7200  # 2 hours in seconds
puts Fbe.sec(build_fact, :duration)  # => "2 hours ago"

Parameters:

  • fact (Factbase::Fact)

    The fact containing the seconds property

  • prop (String, Symbol) (defaults to: :seconds)

    The property name with seconds (defaults to :seconds)

Returns:

  • (String)

    Human-readable time interval (e.g., “2 weeks ago”, “3 hours ago”)

Raises:

  • (RuntimeError)

    If the specified property doesn’t exist in the fact



25
26
27
28
29
30
# File 'lib/fbe/sec.rb', line 25

def Fbe.sec(fact, prop = :seconds)
  s = fact[prop.to_s]
  raise "There is no #{prop.inspect} property" if s.nil?
  s = s.first.to_i
  (Time.now + s).ago
end

.unmask_repos(options: $options, global: $global, loog: $loog, quota_aware: true) ⇒ Array<String>

Note:

Exclusion patterns must start with ‘-’ (e.g., ‘-org/pattern*’)

Note:

Results are shuffled to distribute load when processing

Resolves repository masks to actual GitHub repository names.

Takes a comma-separated list of repository masks from options and expands wildcards by querying GitHub API. Supports inclusion and exclusion patterns. Archived repositories are automatically filtered out.

Examples:

Basic usage with wildcards

# options.repositories = "zerocracy/fbe,zerocracy/ab*"
repos = Fbe.unmask_repos
# => ["zerocracy/fbe", "zerocracy/abc", "zerocracy/abcd"]

Using exclusion patterns

# options.repositories = "zerocracy/*,-zerocracy/private*"
repos = Fbe.unmask_repos
# Returns all zerocracy repos except those starting with 'private'

Empty result handling

# options.repositories = "nonexistent/*"
Fbe.unmask_repos  # Raises error: "No repos found matching: nonexistent/*"

Parameters:

  • options (Judges::Options) (defaults to: $options)

    Options containing ‘repositories’ field with masks

  • global (Hash) (defaults to: $global)

    Global cache for storing API responses

  • loog (Loog) (defaults to: $loog)

    Logger for debug output

  • Boolean (quota_aware)

    Should we stop if quota is off?

Returns:

  • (Array<String>)

    Shuffled list of repository full names (e.g., ‘org/repo’)

Raises:

  • (RuntimeError)

    If no repositories match the provided masks



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/fbe/unmask_repos.rb', line 57

def Fbe.unmask_repos(options: $options, global: $global, loog: $loog, quota_aware: true)
  raise 'Repositories mask is not specified' unless options.repositories
  raise 'Repositories mask is empty' if options.repositories.empty?
  repos = []
  octo = Fbe.octo(loog:, global:, options:)
  masks = (options.repositories || '').split(',')
  masks.reject { |m| m.start_with?('-') }.each do |mask|
    unless mask.include?('*')
      repos << mask
      next
    end
    re = Fbe.mask_to_regex(mask)
    octo.repositories(mask.split('/')[0]).each do |r|
      repos << r[:full_name] if re.match?(r[:full_name])
    end
  end
  masks.select { |m| m.start_with?('-') }.each do |mask|
    re = Fbe.mask_to_regex(mask[1..])
    repos.reject! { |r| re.match?(r) }
  end
  repos.reject! { |repo| octo.repository(repo)[:archived] }
  raise "No repos found matching: #{options.repositories.inspect}" if repos.empty?
  repos.shuffle!
  loog.debug("Scanning #{repos.size} repositories: #{repos.joined}...")
  repos.each { |repo| octo.repository(repo) }
  return repos unless block_given?
  repos.each do |repo|
    if quota_aware && octo.off_quota?
      $loog.info("No GitHub quota left, it is time to stop at #{repo}")
      break
    end
    yield repo
  end
end

.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String

Note:

Results are cached to reduce GitHub API calls

Note:

Subject to GitHub API rate limits

Converts a GitHub user ID into a formatted username string.

The ID of the user (integer) is expected to be stored in the who property of the provided fact. This function makes a live request to GitHub API to retrieve the username. The result is cached globally to minimize API calls. For example, the ID 526301 will be converted to “@yegor256”.

Examples:

Convert user ID to username

contributor = fb.query('(eq type "contributor")').first
contributor.author_id = 526301
puts Fbe.who(contributor, :author_id)  # => "@yegor256"

Parameters:

  • fact (Factbase::Fact)

    The fact containing the GitHub user ID

  • prop (String, Symbol) (defaults to: :who)

    The property name with the ID (defaults to :who)

  • options (Judges::Options) (defaults to: $options)

    The options from judges tool (uses $options global)

  • global (Hash) (defaults to: $global)

    The hash for global caching (uses $global)

  • loog (Loog) (defaults to: $loog)

    The logging facility (uses $loog global)

Returns:

  • (String)

    Formatted username with @ prefix (e.g., “@yegor256”)

Raises:

  • (RuntimeError)

    If the specified property doesn’t exist in the fact



29
30
31
32
33
34
# File 'lib/fbe/who.rb', line 29

def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
  id = fact[prop.to_s]
  raise "There is no #{prop.inspect} property" if id.nil?
  id = id.first.to_i
  "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(id)}"
end