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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +279 -27
- data/doc/Branchproof/Analyzer.md +2 -2
- data/doc/Branchproof/CLI.md +4 -0
- data/doc/Branchproof/ComparisonReport.md +4 -0
- data/doc/Branchproof/Constraints/Solver.md +21 -0
- data/doc/Branchproof/Constraints.md +98 -0
- data/doc/Branchproof/CoverageIndex.md +6 -0
- data/doc/Branchproof/DecisionSyntax.md +22 -0
- data/doc/Branchproof/DecisionTable.md +153 -0
- data/doc/Branchproof/FlowInstrumentation.md +7 -0
- data/doc/Branchproof/FocusedReport.md +5 -2
- data/doc/Branchproof/Instrumenter.md +1 -0
- data/doc/Branchproof/Loader.md +0 -4
- data/doc/Branchproof/MinitestAdapter.md +0 -3
- data/doc/Branchproof/Report.md +18 -0
- data/doc/Branchproof/Runtime.md +23 -0
- data/doc/Branchproof/RuntimeFlow.md +27 -0
- data/doc/Branchproof/SavedReport.md +21 -0
- data/doc/Branchproof/Source.md +1 -0
- data/doc/Branchproof.md +9 -2
- data/doc/CHANGELOG.md +45 -0
- data/doc/README.md +279 -27
- data/lib/branchproof/analyzer.rb +222 -54
- data/lib/branchproof/cli.rb +28 -19
- data/lib/branchproof/comparison.rb +207 -19
- data/lib/branchproof/comparison_report.rb +49 -1
- data/lib/branchproof/constraints.rb +363 -0
- data/lib/branchproof/coverage_index.rb +120 -3
- data/lib/branchproof/decision_syntax.rb +310 -0
- data/lib/branchproof/decision_table.rb +377 -0
- data/lib/branchproof/evidence.rb +101 -36
- data/lib/branchproof/flow_instrumentation.rb +107 -0
- data/lib/branchproof/focused_report.rb +161 -26
- data/lib/branchproof/instrumenter.rb +65 -39
- data/lib/branchproof/limits.rb +4 -1
- data/lib/branchproof/loader.rb +18 -6
- data/lib/branchproof/minimizer.rb +18 -13
- data/lib/branchproof/minitest_adapter.rb +11 -16
- data/lib/branchproof/records.rb +2 -0
- data/lib/branchproof/report.rb +470 -66
- data/lib/branchproof/runtime.rb +25 -30
- data/lib/branchproof/runtime_flow.rb +58 -0
- data/lib/branchproof/saved_report.rb +438 -13
- data/lib/branchproof/source.rb +239 -41
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +1 -4
- data/lib/branchproof.rb +2 -0
- data/llms.txt +9 -2
- data/sig/branchproof.rbs +54 -1
- metadata +12 -1
data/lib/branchproof/runtime.rb
CHANGED
|
@@ -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:
|
|
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] << [
|
|
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] ==
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
current =
|
|
137
|
-
|
|
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] ==
|
|
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
|