scjson 0.3.3 → 0.3.5

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.
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent Name: ruby-engine
4
+ #
5
+ # Part of the scjson project.
6
+ # Developed by Softoboros Technology Inc.
7
+ # Licensed under the BSD 1-Clause License.
8
+
9
+ require 'json'
10
+ require_relative 'engine/context'
11
+
12
+ module Scjson
13
+ #
14
+ # Engine interface to emit standardized JSONL execution traces.
15
+ #
16
+ # This is a contract-level stub that preserves the CLI and trace schema
17
+ # while the full runtime is being implemented. It mirrors Python flags and
18
+ # behavior where appropriate, following Ruby idioms.
19
+ #
20
+ module Engine
21
+ module_function
22
+
23
+ ##
24
+ # Emit a standardized JSONL trace for the given document and event stream.
25
+ #
26
+ # @param input_path [String] Path to SCXML or SCJSON document.
27
+ # @param events_path [String, nil] Path to JSONL event stream (reads STDIN when nil).
28
+ # @param out_path [String, nil] Destination file for trace (writes STDOUT when nil).
29
+ # @param xml [Boolean] When true, treat the input as SCXML (placeholder for future).
30
+ # @param leaf_only [Boolean] Restrict states to leaves (placeholder; no-op in stub).
31
+ # @param omit_actions [Boolean] Omit actionLog entries.
32
+ # @param omit_delta [Boolean] Omit datamodelDelta entries.
33
+ # @param omit_transitions [Boolean] Omit firedTransitions entries.
34
+ # @param advance_time [Float] Advance engine time before processing events (no-op in stub).
35
+ # @param ordering [String] Ordering policy (tolerant|strict|scion); placeholder in stub.
36
+ # @param max_steps [Integer, nil] Limit processed steps (nil = unlimited).
37
+ # @return [void]
38
+ def trace(input_path:,
39
+ events_path: nil,
40
+ out_path: nil,
41
+ xml: false,
42
+ leaf_only: false,
43
+ omit_actions: false,
44
+ omit_delta: false,
45
+ omit_transitions: false,
46
+ advance_time: 0.0,
47
+ ordering: 'tolerant',
48
+ max_steps: nil,
49
+ strip_step0_noise: false,
50
+ strip_step0_states: false,
51
+ keep_cond: false,
52
+ defer_done: true)
53
+ sink = out_path ? File.open(out_path, 'w', encoding: 'utf-8') : $stdout
54
+ begin
55
+ ctx = DocumentContext.from_file(input_path, xml: xml)
56
+ begin
57
+ ctx.ordering_mode = (ordering || 'tolerant')
58
+ rescue StandardError
59
+ # ignore if not supported
60
+ end
61
+ begin
62
+ ctx.defer_done = !!defer_done
63
+ rescue StandardError
64
+ # ignore if not supported
65
+ end
66
+ leaves = leaf_only ? ctx.leaf_state_ids : nil
67
+ # Step 0 snapshot
68
+ init = ctx.trace_init
69
+ if leaf_only && leaves
70
+ %w[configuration enteredStates exitedStates].each do |k|
71
+ init[k] = (init[k] || []).select { |sid| leaves.include?(sid) }
72
+ end
73
+ end
74
+ init['actionLog'] = [] if omit_actions
75
+ init['datamodelDelta'] = {} if omit_delta
76
+ init['firedTransitions'] = [] if omit_transitions
77
+ if strip_step0_noise
78
+ init['datamodelDelta'] = {}
79
+ init['firedTransitions'] = []
80
+ end
81
+ if strip_step0_states
82
+ init['enteredStates'] = []
83
+ init['exitedStates'] = []
84
+ end
85
+ sink.write(JSON.generate({ step: 0 }.merge(init)) + "\n")
86
+
87
+ # Stream of events: from file or STDIN
88
+ stream = events_path ? File.open(events_path, 'r', encoding: 'utf-8') : $stdin
89
+ # Apply global advance_time before first event if provided
90
+ if advance_time && advance_time.to_f > 0
91
+ begin
92
+ ctx.advance_time(advance_time.to_f)
93
+ rescue StandardError
94
+ # ignore
95
+ end
96
+ end
97
+ step_no = 1
98
+ stream.each_line do |line|
99
+ line = line.strip
100
+ next if line.empty?
101
+ begin
102
+ msg = JSON.parse(line)
103
+ rescue StandardError
104
+ next
105
+ end
106
+ # Control token: advance_time -> skip trace emission, but flush timers
107
+ if msg.is_a?(Hash) && msg.key?('advance_time')
108
+ begin
109
+ adv = msg['advance_time']
110
+ ctx.advance_time(adv.to_f)
111
+ # After advancing time, flush any pending timers by running a synthetic step.
112
+ # Only emit a step if something actually changed (entered/exited/fired).
113
+ rec = ctx.trace_step(name: '__time__', data: nil)
114
+ if leaf_only && leaves
115
+ %w[configuration enteredStates exitedStates].each do |k|
116
+ rec[k] = (rec[k] || []).select { |sid| leaves.include?(sid) }
117
+ end
118
+ end
119
+ rec['event'] = nil # hide synthetic event name
120
+ rec['actionLog'] = [] if omit_actions
121
+ unless omit_delta
122
+ if rec['datamodelDelta'].is_a?(Hash)
123
+ dm = rec['datamodelDelta']
124
+ rec['datamodelDelta'] = dm.keys.sort.each_with_object({}) { |k, h| h[k] = dm[k] }
125
+ end
126
+ else
127
+ rec['datamodelDelta'] = {}
128
+ end
129
+ unless keep_cond
130
+ if rec['firedTransitions'].is_a?(Array)
131
+ rec['firedTransitions'] = rec['firedTransitions'].map do |ft|
132
+ if ft.is_a?(Hash)
133
+ ft['cond'] = nil
134
+ end
135
+ ft
136
+ end
137
+ end
138
+ end
139
+ rec['firedTransitions'] = [] if omit_transitions
140
+ sink.write(JSON.generate({ step: step_no }.merge(rec)) + "\n")
141
+ step_no += 1
142
+ rescue StandardError
143
+ # ignore malformed
144
+ end
145
+ next
146
+ end
147
+ break if max_steps && step_no > max_steps
148
+ evt_name = (msg.is_a?(Hash) && (msg['event'] || msg['name']))
149
+ next unless evt_name
150
+ evt_data = msg.is_a?(Hash) ? msg['data'] : nil
151
+ rec = ctx.trace_step(name: evt_name.to_s, data: evt_data)
152
+ if leaf_only && leaves
153
+ %w[configuration enteredStates exitedStates].each do |k|
154
+ rec[k] = (rec[k] || []).select { |sid| leaves.include?(sid) }
155
+ end
156
+ end
157
+ rec['actionLog'] = [] if omit_actions
158
+ # sort datamodelDelta keys for deterministic output
159
+ unless omit_delta
160
+ if rec['datamodelDelta'].is_a?(Hash)
161
+ dm = rec['datamodelDelta']
162
+ rec['datamodelDelta'] = dm.keys.sort.each_with_object({}) { |k, h| h[k] = dm[k] }
163
+ end
164
+ else
165
+ rec['datamodelDelta'] = {}
166
+ end
167
+ # scrub cond in firedTransitions unless requested
168
+ unless keep_cond
169
+ if rec['firedTransitions'].is_a?(Array)
170
+ rec['firedTransitions'] = rec['firedTransitions'].map do |ft|
171
+ if ft.is_a?(Hash)
172
+ ft['cond'] = nil
173
+ end
174
+ ft
175
+ end
176
+ end
177
+ end
178
+ rec['firedTransitions'] = [] if omit_transitions
179
+ sink.write(JSON.generate({ step: step_no }.merge(rec)) + "\n")
180
+ step_no += 1
181
+ end
182
+ ensure
183
+ sink.close if sink && sink != $stdout
184
+ end
185
+ end
186
+ end
187
+ end