bitfab 0.64.6 → 0.64.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bitfab/cloudReplay.py +3 -0
- data/lib/bitfab/http_client.rb +3 -1
- data/lib/bitfab/labels.rb +36 -7
- data/lib/bitfab/replay.rb +12 -0
- data/lib/bitfab/replay_invocation.rb +113 -0
- data/lib/bitfab/replay_registry.rb +24 -6
- data/lib/bitfab/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6999d067c0ee15b7098bd4a432de082c0993b403178de77fd3a71c00ec6d1558
|
|
4
|
+
data.tar.gz: 5fe41dfe83313ca0413369680a25aaf09244a25dd40944b7bb0bc035267e7cfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 691bf9d5734641198986549b3888d0b488374a21fc58daf80d6afe2e19a803b6f8e76ccc7974cd4abe6969993ff6655a515384f73aeaa4e26a4044af6bc93f7d
|
|
7
|
+
data.tar.gz: bd9a1aa20a2fcf7937daacb44022e1f6b4211bda69f145e075c5ad16c408417e5571f948016901118be5cb211d094c11d7c349f4dca6ecf82ba7d011a6aeca26
|
data/lib/bitfab/cloudReplay.py
CHANGED
|
@@ -33,6 +33,8 @@ API_VERSION = "2026-03-10"
|
|
|
33
33
|
REPLAY_COMMAND_ENV = "BITFAB_REPLAY_COMMAND"
|
|
34
34
|
SDK_LANGUAGE_ENV = "BITFAB_SDK_LANGUAGE"
|
|
35
35
|
CHECK_COMMAND_ENV = "BITFAB_REPLAY_CHECK"
|
|
36
|
+
EXECUTION_TARGET_ENV = "BITFAB_REPLAY_EXECUTION_TARGET"
|
|
37
|
+
HOSTED_EXECUTION_TARGET = "hosted"
|
|
36
38
|
REQUEST_VERSION = 3
|
|
37
39
|
OLDEST_DISPATCHED_VERSION = 2
|
|
38
40
|
RESULT_LINE = "bitfab-replay-result "
|
|
@@ -1776,6 +1778,7 @@ def run_command(directory, args, timeout, experiment, *, summarize=True):
|
|
|
1776
1778
|
with subprocess.Popen(
|
|
1777
1779
|
args,
|
|
1778
1780
|
cwd=directory,
|
|
1781
|
+
env={**os.environ, EXECUTION_TARGET_ENV: HOSTED_EXECUTION_TARGET},
|
|
1779
1782
|
stdout=output,
|
|
1780
1783
|
stderr=subprocess.PIPE,
|
|
1781
1784
|
stdin=subprocess.DEVNULL,
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
|
|
7
7
|
require_relative "git_state"
|
|
8
|
+
require_relative "replay_invocation"
|
|
8
9
|
require_relative "compress"
|
|
9
10
|
require_relative "connection_pool"
|
|
10
11
|
require_relative "constants"
|
|
@@ -262,7 +263,7 @@ module Bitfab
|
|
|
262
263
|
# same org and trace function or the server rejects the replay
|
|
263
264
|
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
|
|
264
265
|
code_change_files: nil, experiment_group_id: nil, name: nil, notes: nil, metadata: nil, include_db_branch_lease: false, dataset_ids: nil,
|
|
265
|
-
grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, skip_assertion_judging: false, include_original_metadata: false, experimental_selective_replay: nil, dry_run: false)
|
|
266
|
+
grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, skip_assertion_judging: false, include_original_metadata: false, experimental_selective_replay: nil, dry_run: false, invocation: nil)
|
|
266
267
|
payload = {
|
|
267
268
|
"traceFunctionKey" => trace_function_key
|
|
268
269
|
}
|
|
@@ -293,6 +294,7 @@ module Bitfab
|
|
|
293
294
|
payload["includeOriginalMetadata"] = true if include_original_metadata
|
|
294
295
|
payload["experimentalSelectiveReplay"] = experimental_selective_replay if experimental_selective_replay
|
|
295
296
|
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
297
|
+
payload["invocation"] = invocation unless invocation.nil?
|
|
296
298
|
git_state = GitState.resolved
|
|
297
299
|
payload["git"] = git_state if git_state
|
|
298
300
|
|
data/lib/bitfab/labels.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Bitfab
|
|
|
14
14
|
@http_client = http_client
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def save(label:, annotation:, trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
18
|
-
confidence: nil, experiment_id: nil,
|
|
17
|
+
def save(label:, annotation:, assertion_id:, trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
18
|
+
confidence: nil, experiment_id: nil, evidence: EVIDENCE_UNSET, test_run_id: nil)
|
|
19
19
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
20
20
|
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
21
21
|
update["label"] = label
|
|
@@ -27,6 +27,7 @@ module Bitfab
|
|
|
27
27
|
|
|
28
28
|
def save_all(labels, experiment_id: nil, test_run_id: nil)
|
|
29
29
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
30
|
+
require_assertion_ids(labels)
|
|
30
31
|
payload = {"labels" => labels}
|
|
31
32
|
unless experiment_id.nil?
|
|
32
33
|
payload["experimentId"] = experiment_id
|
|
@@ -35,7 +36,7 @@ module Bitfab
|
|
|
35
36
|
@http_client.request(LABELS_PATH, payload)["labels"]
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
def skip(trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil,
|
|
39
|
+
def skip(assertion_id:, trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil,
|
|
39
40
|
test_run_id: nil, annotation: nil)
|
|
40
41
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
41
42
|
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:).merge("skip" => true)
|
|
@@ -49,16 +50,15 @@ module Bitfab
|
|
|
49
50
|
save_all([update.merge("archive" => true)], experiment_id:).first
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def save_human(label:, annotation:, trace_id:, confidence: nil,
|
|
53
|
-
|
|
54
|
-
update = {"traceId" => trace_id, "label" => label, "annotation" => annotation}
|
|
55
|
-
update["assertionId"] = assertion_id unless assertion_id.nil?
|
|
53
|
+
def save_human(label:, annotation:, trace_id:, assertion_id:, confidence: nil, evidence: EVIDENCE_UNSET)
|
|
54
|
+
update = {"traceId" => trace_id, "assertionId" => assertion_id, "label" => label, "annotation" => annotation}
|
|
56
55
|
update["confidence"] = confidence unless confidence.nil?
|
|
57
56
|
update["evidence"] = evidence unless evidence.equal?(EVIDENCE_UNSET)
|
|
58
57
|
save_human_all([update]).first
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def save_human_all(labels)
|
|
61
|
+
require_human_assertion_ids(labels)
|
|
62
62
|
@http_client.request(HUMAN_LABELS_PATH, {"labels" => labels})["labels"]
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -83,6 +83,35 @@ module Bitfab
|
|
|
83
83
|
|
|
84
84
|
private
|
|
85
85
|
|
|
86
|
+
def require_assertion_ids(labels)
|
|
87
|
+
unscoped = labels.reject do |entry|
|
|
88
|
+
entry_value(entry, "archive") == true || !blank?(entry_value(entry, "assertionId"))
|
|
89
|
+
end
|
|
90
|
+
return if unscoped.empty?
|
|
91
|
+
|
|
92
|
+
targets = unscoped.map { |entry| entry_value(entry, "traceId") || entry_value(entry, "originalTraceId") }
|
|
93
|
+
raise ArgumentError, "Agent verdicts must name an assertion: #{targets.join(", ")}. " \
|
|
94
|
+
"Pass the assertion_id of the assertion each verdict scores to labels.save, save_all, and skip. " \
|
|
95
|
+
"Only archive may omit it, to clear an old whole-trace verdict."
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def require_human_assertion_ids(labels)
|
|
99
|
+
unscoped = labels.select { |entry| blank?(entry_value(entry, "assertionId")) }
|
|
100
|
+
return if unscoped.empty?
|
|
101
|
+
|
|
102
|
+
targets = unscoped.map { |entry| entry_value(entry, "traceId") }
|
|
103
|
+
raise ArgumentError, "Human verdicts must name an assertion: #{targets.join(", ")}. " \
|
|
104
|
+
"Pass the assertion_id of the assertion each verdict scores to labels.save_human and save_human_all."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def entry_value(entry, key)
|
|
108
|
+
entry.key?(key) ? entry[key] : entry[key.to_sym]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def blank?(value)
|
|
112
|
+
value.nil? || value.to_s.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
86
115
|
def label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
87
116
|
if trace_id.nil? == original_trace_id.nil?
|
|
88
117
|
raise ArgumentError, "Pass exactly one of trace_id or original_trace_id. " \
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "timeout"
|
|
|
8
8
|
require_relative "constants"
|
|
9
9
|
require_relative "git_command"
|
|
10
10
|
require_relative "mock_override"
|
|
11
|
+
require_relative "replay_invocation"
|
|
11
12
|
require_relative "serialize"
|
|
12
13
|
require_relative "selective_replay"
|
|
13
14
|
require_relative "traceable"
|
|
@@ -142,6 +143,8 @@ module Bitfab
|
|
|
142
143
|
# Replay historical traces through a traced method and create an experiment.
|
|
143
144
|
module Replay
|
|
144
145
|
CODE_CHANGE_UNSET = Object.new.freeze
|
|
146
|
+
UNSET_DEFAULT = Object.new.freeze
|
|
147
|
+
OPTION_DEFAULTS = {mock: "marked"}.freeze
|
|
145
148
|
PERSISTENCE_TIMEOUT_SECONDS = 30.0
|
|
146
149
|
PERSISTENCE_POLL_SECONDS = 0.1
|
|
147
150
|
JUDGE_ASSERTIONS_DEPRECATION = "judge_assertions is deprecated. Assertion judging is on by default; " \
|
|
@@ -193,6 +196,13 @@ module Bitfab
|
|
|
193
196
|
raise error, cause:
|
|
194
197
|
end
|
|
195
198
|
|
|
199
|
+
def explicit_replay_options(options)
|
|
200
|
+
options.reject do |option, value|
|
|
201
|
+
value.nil? || value == false || value.equal?(ReplayConcurrency::UNSET) ||
|
|
202
|
+
value.equal?(CODE_CHANGE_UNSET) || value == OPTION_DEFAULTS.fetch(option, UNSET_DEFAULT)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
196
206
|
# Replay historical traces through a method and create an experiment.
|
|
197
207
|
#
|
|
198
208
|
# Fetches the last N traces for the given trace function key, re-runs each
|
|
@@ -289,6 +299,7 @@ module Bitfab
|
|
|
289
299
|
dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, skip_assertion_judging: false, judge_assertions: nil, dry_run: false,
|
|
290
300
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
|
|
291
301
|
on_experiment_start: nil, experimental_selective_replay: nil)
|
|
302
|
+
explicit_options = method(:run).parameters.filter_map { |type, option| [option, binding.local_variable_get(option)] if type == :key }
|
|
292
303
|
skip_assertion_judging = resolve_skip_assertion_judging(skip_assertion_judging, judge_assertions)
|
|
293
304
|
concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
|
|
294
305
|
attempts = concurrency_options.attempts
|
|
@@ -396,6 +407,7 @@ module Bitfab
|
|
|
396
407
|
include_original_metadata: !adapt_inputs.nil?,
|
|
397
408
|
db_branch_settings: resolved_db_branch_settings,
|
|
398
409
|
dry_run:,
|
|
410
|
+
invocation: ReplayInvocation.safe_build { ReplayInvocation.build(ReplayInvocation.current_cli_flags || ReplayInvocation.option_flag_entries(explicit_replay_options(explicit_options))) },
|
|
399
411
|
**(experimental_selective_replay ? {experimental_selective_replay: SelectiveReplay.wire_options(experimental_selective_replay)} : {})
|
|
400
412
|
)
|
|
401
413
|
experiment_id = replay_data["experimentId"] || replay_data["testRunId"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
module ReplayInvocation
|
|
7
|
+
EXECUTION_TARGET_ENV = "BITFAB_REPLAY_EXECUTION_TARGET"
|
|
8
|
+
HOSTED_EXECUTION_TARGET = "hosted"
|
|
9
|
+
CI_ENV = "CI"
|
|
10
|
+
CI_TRUE_VALUES = %w[true 1].freeze
|
|
11
|
+
CI_PRESENCE_ENV_VARS = %w[GITHUB_ACTIONS GITLAB_CI CIRCLECI BUILDKITE JENKINS_URL TF_BUILD].freeze
|
|
12
|
+
MAX_FLAGS = 100
|
|
13
|
+
MAX_FLAG_NAME_LENGTH = 100
|
|
14
|
+
MAX_FLAG_STRING_LENGTH = 1_000
|
|
15
|
+
MAX_FLAG_LIST_LENGTH = 100
|
|
16
|
+
SECRET_FLAG_NAME = /key|token|secret|password/i
|
|
17
|
+
CLI_FLAGS_KEY = :bitfab_replay_cli_flags
|
|
18
|
+
UNSUPPORTED = Object.new.freeze
|
|
19
|
+
METADATA_FLAG = "metadata"
|
|
20
|
+
OPTION_FLAG_NAMES = {
|
|
21
|
+
max_concurrency: "concurrency",
|
|
22
|
+
dataset_id: "dataset-ids",
|
|
23
|
+
dataset_ids: "dataset-ids"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def environment
|
|
29
|
+
return "cloud_replay" if ENV[EXECUTION_TARGET_ENV] == HOSTED_EXECUTION_TARGET
|
|
30
|
+
return "ci" if CI_TRUE_VALUES.include?(ENV.fetch(CI_ENV, "").downcase)
|
|
31
|
+
return "ci" if CI_PRESENCE_ENV_VARS.any? { |name| !ENV.fetch(name, "").empty? }
|
|
32
|
+
|
|
33
|
+
"local"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def flag_scalar(value)
|
|
37
|
+
case value
|
|
38
|
+
when String then value[0, MAX_FLAG_STRING_LENGTH]
|
|
39
|
+
when true, false, nil then value
|
|
40
|
+
when Integer then value
|
|
41
|
+
when Float then value.finite? ? value : UNSUPPORTED
|
|
42
|
+
when Symbol then value.to_s[0, MAX_FLAG_STRING_LENGTH]
|
|
43
|
+
else UNSUPPORTED
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flag_value(value)
|
|
48
|
+
if value.is_a?(Array)
|
|
49
|
+
items = value.first(MAX_FLAG_LIST_LENGTH).map { |item| flag_scalar(item) }
|
|
50
|
+
return (items.any? { |item| item.equal?(UNSUPPORTED) }) ? true : items
|
|
51
|
+
end
|
|
52
|
+
scalar = flag_scalar(value)
|
|
53
|
+
scalar.equal?(UNSUPPORTED) || scalar
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def option_flag_name(option)
|
|
57
|
+
OPTION_FLAG_NAMES.fetch(option.to_sym) { option.to_s.tr("_", "-") }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def option_flag_entries(options)
|
|
61
|
+
mapped, rest = options.to_a.partition { |option, _value| OPTION_FLAG_NAMES.key?(option.to_sym) }
|
|
62
|
+
(mapped + rest).map { |option, value| [option_flag_name(option), value] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def metadata_keys(value)
|
|
66
|
+
keys = case value
|
|
67
|
+
when Hash then value.keys
|
|
68
|
+
when Array then value.map { |item| item.is_a?(Array) ? item.first : item.to_s.split("=", 2).first }
|
|
69
|
+
else return true
|
|
70
|
+
end
|
|
71
|
+
keys.map(&:to_s).uniq.sort
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def flags(entries)
|
|
75
|
+
result = {}
|
|
76
|
+
entries.each do |name, value|
|
|
77
|
+
break if result.length >= MAX_FLAGS
|
|
78
|
+
|
|
79
|
+
name = name.to_s
|
|
80
|
+
next if name.empty? || result.key?(name) || name.length > MAX_FLAG_NAME_LENGTH || SECRET_FLAG_NAME.match?(name)
|
|
81
|
+
|
|
82
|
+
result[name] = flag_value((name == METADATA_FLAG) ? metadata_keys(value) : value)
|
|
83
|
+
end
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build(entries = {})
|
|
88
|
+
{
|
|
89
|
+
"flags" => flags(entries),
|
|
90
|
+
"sdk" => {"language" => "ruby", "version" => VERSION},
|
|
91
|
+
"environment" => environment
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def safe_build
|
|
96
|
+
yield
|
|
97
|
+
rescue
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current_cli_flags
|
|
102
|
+
Thread.current[CLI_FLAGS_KEY]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def with_cli_flags(cli_flags)
|
|
106
|
+
previous = Thread.current[CLI_FLAGS_KEY]
|
|
107
|
+
Thread.current[CLI_FLAGS_KEY] = cli_flags
|
|
108
|
+
yield
|
|
109
|
+
ensure
|
|
110
|
+
Thread.current[CLI_FLAGS_KEY] = previous
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -88,6 +88,7 @@ module Bitfab
|
|
|
88
88
|
# Argument parsing and output for the SDK-owned replay command.
|
|
89
89
|
module ReplayCli
|
|
90
90
|
MAX_PINNED_TRACE_IDS = 100
|
|
91
|
+
CLI_INVOCATION_SKIPPED = %i[pipeline seed run from_trace].freeze
|
|
91
92
|
|
|
92
93
|
class ItemsErroredError < StandardError; end
|
|
93
94
|
|
|
@@ -247,12 +248,14 @@ module Bitfab
|
|
|
247
248
|
stderr.puts "[replay] Experiment #{experiment[:experiment_id]}: #{experiment[:experiment_url]}"
|
|
248
249
|
end
|
|
249
250
|
result = begin
|
|
250
|
-
|
|
251
|
-
registration.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
ReplayInvocation.with_cli_flags(cli_invocation_flags(args)) do
|
|
252
|
+
registration.client.replay(
|
|
253
|
+
registration.receiver,
|
|
254
|
+
registration.method_name,
|
|
255
|
+
trace_function_key: registration.trace_function_key,
|
|
256
|
+
**options
|
|
257
|
+
)
|
|
258
|
+
end
|
|
256
259
|
rescue ReplayError => error
|
|
257
260
|
error.items.each do |item|
|
|
258
261
|
item_error = item[:trace_error] || item[:replay_error] || item[:error]
|
|
@@ -273,6 +276,21 @@ module Bitfab
|
|
|
273
276
|
result
|
|
274
277
|
end
|
|
275
278
|
|
|
279
|
+
def cli_invocation_flags(args)
|
|
280
|
+
args.except(*CLI_INVOCATION_SKIPPED).each_with_object({}) do |(key, value), flags|
|
|
281
|
+
case key
|
|
282
|
+
when :param
|
|
283
|
+
flags["param"] = value.map { |raw| raw.split("=", 2).first }
|
|
284
|
+
when :max_concurrency
|
|
285
|
+
flags["concurrency"] = value
|
|
286
|
+
when :db_branch, :memory_throttle
|
|
287
|
+
flags["#{"no-" unless value}#{key.to_s.tr("_", "-")}"] = true
|
|
288
|
+
else
|
|
289
|
+
flags[key.to_s.tr("_", "-")] = value
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
276
294
|
def fail_if_items_errored(result, dry_run:)
|
|
277
295
|
items = result[:items].reject { |item| item[:carried_over] }
|
|
278
296
|
errored = items.count { |item| item[:error] }
|
data/lib/bitfab/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitfab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.64.
|
|
4
|
+
version: 0.64.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -184,6 +184,7 @@ files:
|
|
|
184
184
|
- lib/bitfab/replay_branch.rb
|
|
185
185
|
- lib/bitfab/replay_cli.rb
|
|
186
186
|
- lib/bitfab/replay_concurrency.rb
|
|
187
|
+
- lib/bitfab/replay_invocation.rb
|
|
187
188
|
- lib/bitfab/replay_memory.rb
|
|
188
189
|
- lib/bitfab/replay_processes.rb
|
|
189
190
|
- lib/bitfab/replay_registry.rb
|