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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb774e94257b1eb736023bb05b20605e97d86ba24218b55288957919789450df
4
- data.tar.gz: 6f63ab2372787edbb1961b964532738fac437ab353c701597187e7e42b696fd8
3
+ metadata.gz: 6999d067c0ee15b7098bd4a432de082c0993b403178de77fd3a71c00ec6d1558
4
+ data.tar.gz: 5fe41dfe83313ca0413369680a25aaf09244a25dd40944b7bb0bc035267e7cfd
5
5
  SHA512:
6
- metadata.gz: 74ec4a06f27ddc8e76832bbf60fa44d60edfbe45d5f8d8b29c82a8892b95d656245327a6c0eb4de1ccadcef0e1b29740cfeb82fae8f0bcc9af2283a0ad40c299
7
- data.tar.gz: 832554fe31418799c0face53a8426eac016214babb6faa8c5be197dbfaa9f4276b69e8de90ae0289ccc061348aaee9e9b6b2254737dd785635722c858014b953
6
+ metadata.gz: 691bf9d5734641198986549b3888d0b488374a21fc58daf80d6afe2e19a803b6f8e76ccc7974cd4abe6969993ff6655a515384f73aeaa4e26a4044af6bc93f7d
7
+ data.tar.gz: bd9a1aa20a2fcf7937daacb44022e1f6b4211bda69f145e075c5ad16c408417e5571f948016901118be5cb211d094c11d7c349f4dca6ecf82ba7d011a6aeca26
@@ -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,
@@ -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, assertion_id: nil, evidence: EVIDENCE_UNSET, test_run_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, assertion_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, assertion_id: nil,
53
- evidence: EVIDENCE_UNSET)
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
- registration.client.replay(
251
- registration.receiver,
252
- registration.method_name,
253
- trace_function_key: registration.trace_function_key,
254
- **options
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] }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.64.6"
4
+ VERSION = "0.64.8"
5
5
  end
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.6
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