branchproof 0.6.0 → 0.8.0

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +279 -27
  4. data/doc/Branchproof/Analyzer.md +2 -2
  5. data/doc/Branchproof/CLI.md +4 -0
  6. data/doc/Branchproof/ComparisonReport.md +4 -0
  7. data/doc/Branchproof/Constraints/Solver.md +21 -0
  8. data/doc/Branchproof/Constraints.md +98 -0
  9. data/doc/Branchproof/CoverageIndex.md +6 -0
  10. data/doc/Branchproof/DecisionSyntax.md +22 -0
  11. data/doc/Branchproof/DecisionTable.md +153 -0
  12. data/doc/Branchproof/FlowInstrumentation.md +7 -0
  13. data/doc/Branchproof/FocusedReport.md +5 -2
  14. data/doc/Branchproof/Instrumenter.md +1 -0
  15. data/doc/Branchproof/Loader.md +0 -4
  16. data/doc/Branchproof/MinitestAdapter.md +0 -3
  17. data/doc/Branchproof/Report.md +18 -0
  18. data/doc/Branchproof/Runtime.md +23 -0
  19. data/doc/Branchproof/RuntimeFlow.md +27 -0
  20. data/doc/Branchproof/SavedReport.md +21 -0
  21. data/doc/Branchproof/Source.md +1 -0
  22. data/doc/Branchproof.md +9 -2
  23. data/doc/CHANGELOG.md +45 -0
  24. data/doc/README.md +279 -27
  25. data/lib/branchproof/analyzer.rb +222 -54
  26. data/lib/branchproof/cli.rb +28 -19
  27. data/lib/branchproof/comparison.rb +207 -19
  28. data/lib/branchproof/comparison_report.rb +49 -1
  29. data/lib/branchproof/constraints.rb +363 -0
  30. data/lib/branchproof/coverage_index.rb +120 -3
  31. data/lib/branchproof/decision_syntax.rb +310 -0
  32. data/lib/branchproof/decision_table.rb +377 -0
  33. data/lib/branchproof/evidence.rb +101 -36
  34. data/lib/branchproof/flow_instrumentation.rb +107 -0
  35. data/lib/branchproof/focused_report.rb +161 -26
  36. data/lib/branchproof/instrumenter.rb +65 -39
  37. data/lib/branchproof/limits.rb +4 -1
  38. data/lib/branchproof/loader.rb +18 -6
  39. data/lib/branchproof/minimizer.rb +18 -13
  40. data/lib/branchproof/minitest_adapter.rb +11 -16
  41. data/lib/branchproof/records.rb +2 -0
  42. data/lib/branchproof/report.rb +470 -66
  43. data/lib/branchproof/runtime.rb +25 -30
  44. data/lib/branchproof/runtime_flow.rb +58 -0
  45. data/lib/branchproof/saved_report.rb +438 -13
  46. data/lib/branchproof/source.rb +239 -41
  47. data/lib/branchproof/version.rb +1 -1
  48. data/lib/branchproof/worker.rb +1 -4
  49. data/lib/branchproof.rb +2 -0
  50. data/llms.txt +9 -2
  51. data/sig/branchproof.rbs +54 -1
  52. metadata +12 -1
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
+ require_relative "runtime_flow"
4
5
 
5
6
  module Branchproof
6
7
  # Process-local execution recorder. It deliberately never coerces or stores
7
8
  # application values: Ruby's conditional expression is used for truthiness.
8
9
  # Captures condition evaluations while preserving application values.
9
10
  module Runtime
11
+ extend RuntimeFlow
12
+
13
+ FRAME_STATE_KEY = :branchproof_runtime_frame_state
14
+
10
15
  class << self
11
16
  def boot(evidence:)
12
17
  return nil if defined?(@booted) && @booted && @evidence.equal?(evidence) && Process.pid == @process_id
@@ -14,8 +19,6 @@ module Branchproof
14
19
  @evidence = evidence
15
20
  @run_id = evidence.respond_to?(:run_id) ? evidence.run_id.to_s : SecureRandom.hex(16)
16
21
  @process_id = Process.pid
17
- @frames = {}
18
- @contexts = {}
19
22
  @diagnostics = []
20
23
  @storage_disabled = false
21
24
  @booted = true
@@ -23,9 +26,8 @@ module Branchproof
23
26
  end
24
27
 
25
28
  def enter(decision_id)
26
- state[:frames] << { decision_id: String(decision_id), context: state[:context]&.dup,
27
- observations: [], outcome: nil, finished: false,
28
- execution_id: SecureRandom.hex(12) }
29
+ state[:frames] << { decision_id: decision_id, context: state[:context]&.dup,
30
+ observations: [], outcome: nil, finished: false }
29
31
  nil
30
32
  end
31
33
 
@@ -33,7 +35,7 @@ module Branchproof
33
35
  frame = current_frame(decision_id)
34
36
  if frame
35
37
  truth = value ? true : false
36
- frame[:observations] << [Integer(index), truth]
38
+ frame[:observations] << [index, truth]
37
39
  end
38
40
  value
39
41
  end
@@ -48,9 +50,10 @@ module Branchproof
48
50
  end
49
51
 
50
52
  def leave(decision_id)
53
+ detect_fork
51
54
  frames = state[:frames]
52
55
  frame = frames.pop
53
- unless frame && frame[:decision_id] == String(decision_id)
56
+ unless frame && frame[:decision_id] == decision_id
54
57
  latch("runtime_frame_mismatch", "decision frame stack is not balanced")
55
58
  cleanup_state
56
59
  return nil
@@ -59,12 +62,10 @@ module Branchproof
59
62
 
60
63
  execution = {
61
64
  run_id: @run_id,
62
- execution_id: frame[:execution_id],
63
65
  decision_id: frame[:decision_id],
64
66
  test_id: frame[:context]&.fetch(:test_id, nil),
65
67
  phase: frame[:context]&.fetch(:phase, "unattributed") || "unattributed",
66
- owner: owner_tuple,
67
- observations: frame[:observations].map(&:dup),
68
+ observations: frame[:observations],
68
69
  outcome: frame[:finished] ? frame[:outcome] : nil,
69
70
  status: frame[:finished] ? "completed" : "aborted"
70
71
  }
@@ -117,38 +118,32 @@ module Branchproof
117
118
  private
118
119
 
119
120
  def state
120
- @frames ||= {}
121
- if @process_id && Process.pid != @process_id
122
- @process_id = Process.pid
123
- @run_id = SecureRandom.hex(16)
124
- @frames = {}
125
- @diagnostics = [{ code: "forked_process", severity: "error",
126
- message: "runtime process identity changed; evidence storage disabled",
127
- source_id: nil, decision_id: nil, execution_id: nil, test_id: nil, details: {} }]
128
- @storage_disabled = true
129
- end
130
- key = [Process.pid, Thread.current, Fiber.current]
131
- @frames[key] ||= { frames: [], context: nil }
121
+ Thread.current[FRAME_STATE_KEY] ||= { frames: [], context: nil }
132
122
  end
133
123
 
134
124
  def cleanup_state
135
- key = [Process.pid, Thread.current, Fiber.current]
136
- current = @frames[key]
137
- @frames.delete(key) if current && current[:frames].empty? && current[:context].nil?
125
+ current = Thread.current[FRAME_STATE_KEY]
126
+ Thread.current[FRAME_STATE_KEY] = nil if current && current[:frames].empty? && current[:context].nil?
127
+ end
128
+
129
+ def detect_fork
130
+ return unless @process_id && Process.pid != @process_id
131
+
132
+ @process_id = Process.pid
133
+ @diagnostics = [{ code: "forked_process", severity: "error",
134
+ message: "runtime process identity changed; evidence storage disabled",
135
+ source_id: nil, decision_id: nil, execution_id: nil, test_id: nil, details: {} }]
136
+ @storage_disabled = true
138
137
  end
139
138
 
140
139
  def current_frame(decision_id)
141
140
  frame = state[:frames].last
142
- return frame if frame && frame[:decision_id] == String(decision_id)
141
+ return frame if frame && frame[:decision_id] == decision_id
143
142
 
144
143
  latch("runtime_frame_mismatch", "no active frame for #{decision_id}") if frame
145
144
  nil
146
145
  end
147
146
 
148
- def owner_tuple
149
- { process_id: Process.pid, thread_id: Thread.current.object_id, fiber_id: Fiber.current.object_id }
150
- end
151
-
152
147
  def safely_record(execution)
153
148
  result = @evidence.record(execution: execution)
154
149
  unless result.is_a?(Hash) && result[:status].to_s == "recorded"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Branchproof
4
+ # Records native Ruby path selection without evaluating application values twice.
5
+ module RuntimeFlow
6
+ def flow_candidate(decision_id, index)
7
+ frame = current_frame(decision_id)
8
+ frame[:observations] << [index, false] if frame
9
+ nil
10
+ end
11
+
12
+ def flow_selected(decision_id)
13
+ frame = current_frame(decision_id)
14
+ if frame && !frame[:observations].empty?
15
+ frame[:observations].last[1] = true
16
+ frame[:outcome] = true
17
+ frame[:finished] = true
18
+ end
19
+ nil
20
+ end
21
+
22
+ def flow_select(decision_id, index)
23
+ frame = current_frame(decision_id)
24
+ if frame
25
+ frame[:observations] = (0..index).map { |position| [position, position == index] }
26
+ frame[:outcome] = true
27
+ frame[:finished] = true
28
+ end
29
+ nil
30
+ end
31
+
32
+ def flow_path(decision_id, index)
33
+ frame = current_frame(decision_id)
34
+ if frame
35
+ frame[:observations] = [[0, index.zero?], [1, index == 1]]
36
+ frame[:outcome] = true
37
+ frame[:finished] = true
38
+ end
39
+ nil
40
+ end
41
+
42
+ def flow_finish(decision_id, value, default_path = nil)
43
+ frame = current_frame(decision_id)
44
+ flow_path(decision_id, default_path) if frame && !frame[:finished] && !default_path.nil?
45
+ value
46
+ end
47
+
48
+ def flow_receiver(decision_id, receiver)
49
+ enter(decision_id)
50
+ begin
51
+ flow_path(decision_id, nil.equal?(receiver) ? 0 : 1)
52
+ ensure
53
+ leave(decision_id)
54
+ end
55
+ receiver
56
+ end
57
+ end
58
+ end