Module: Scjson::Engine

Defined in:
lib/scjson/engine.rb,
lib/scjson/engine/context.rb

Overview

Engine interface to emit standardized JSONL execution traces.

This is a contract-level stub that preserves the CLI and trace schema while the full runtime is being implemented. It mirrors Python flags and behavior where appropriate, following Ruby idioms.

Defined Under Namespace

Classes: DocumentContext

Class Method Summary collapse

Class Method Details

.trace(input_path:, events_path: nil, out_path: nil, xml: false, leaf_only: false, omit_actions: false, omit_delta: false, omit_transitions: false, advance_time: 0.0, ordering: 'tolerant', max_steps: nil, strip_step0_noise: false, strip_step0_states: false, keep_cond: false, defer_done: true) ⇒ void

This method returns an undefined value.

Emit a standardized JSONL trace for the given document and event stream.

Parameters:

  • input_path (String)

    Path to SCXML or SCJSON document.

  • events_path (String, nil) (defaults to: nil)

    Path to JSONL event stream (reads STDIN when nil).

  • out_path (String, nil) (defaults to: nil)

    Destination file for trace (writes STDOUT when nil).

  • xml (Boolean) (defaults to: false)

    When true, treat the input as SCXML (placeholder for future).

  • leaf_only (Boolean) (defaults to: false)

    Restrict states to leaves (placeholder; no-op in stub).

  • omit_actions (Boolean) (defaults to: false)

    Omit actionLog entries.

  • omit_delta (Boolean) (defaults to: false)

    Omit datamodelDelta entries.

  • omit_transitions (Boolean) (defaults to: false)

    Omit firedTransitions entries.

  • advance_time (Float) (defaults to: 0.0)

    Advance engine time before processing events (no-op in stub).

  • ordering (String) (defaults to: 'tolerant')

    Ordering policy (tolerant|strict|scion); placeholder in stub.

  • max_steps (Integer, nil) (defaults to: nil)

    Limit processed steps (nil = unlimited).



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/scjson/engine.rb', line 38

def trace(input_path:,
          events_path: nil,
          out_path: nil,
          xml: false,
          leaf_only: false,
          omit_actions: false,
          omit_delta: false,
          omit_transitions: false,
          advance_time: 0.0,
          ordering: 'tolerant',
          max_steps: nil,
          strip_step0_noise: false,
          strip_step0_states: false,
          keep_cond: false,
          defer_done: true)
  sink = out_path ? File.open(out_path, 'w', encoding: 'utf-8') : $stdout
  begin
    ctx = DocumentContext.from_file(input_path, xml: xml)
    begin
      ctx.ordering_mode = (ordering || 'tolerant')
    rescue StandardError
      # ignore if not supported
    end
    begin
      ctx.defer_done = !!defer_done
    rescue StandardError
      # ignore if not supported
    end
    leaves = leaf_only ? ctx.leaf_state_ids : nil
    # Step 0 snapshot
    init = ctx.trace_init
    if leaf_only && leaves
      %w[configuration enteredStates exitedStates].each do |k|
        init[k] = (init[k] || []).select { |sid| leaves.include?(sid) }
      end
    end
    init['actionLog'] = [] if omit_actions
    init['datamodelDelta'] = {} if omit_delta
    init['firedTransitions'] = [] if omit_transitions
    if strip_step0_noise
      init['datamodelDelta'] = {}
      init['firedTransitions'] = []
    end
    if strip_step0_states
      init['enteredStates'] = []
      init['exitedStates'] = []
    end
    sink.write(JSON.generate({ step: 0 }.merge(init)) + "\n")

    # Stream of events: from file or STDIN
    stream = events_path ? File.open(events_path, 'r', encoding: 'utf-8') : $stdin
    # Apply global advance_time before first event if provided
    if advance_time && advance_time.to_f > 0
      begin
        ctx.advance_time(advance_time.to_f)
      rescue StandardError
        # ignore
      end
    end
    step_no = 1
    stream.each_line do |line|
      line = line.strip
      next if line.empty?
      begin
        msg = JSON.parse(line)
      rescue StandardError
        next
      end
      # Control token: advance_time -> skip trace emission, but flush timers
      if msg.is_a?(Hash) && msg.key?('advance_time')
        begin
          adv = msg['advance_time']
          ctx.advance_time(adv.to_f)
          # After advancing time, flush any pending timers by running a synthetic step.
          # Only emit a step if something actually changed (entered/exited/fired).
          rec = ctx.trace_step(name: '__time__', data: nil)
            if leaf_only && leaves
              %w[configuration enteredStates exitedStates].each do |k|
                rec[k] = (rec[k] || []).select { |sid| leaves.include?(sid) }
              end
            end
            rec['event'] = nil # hide synthetic event name
            rec['actionLog'] = [] if omit_actions
            unless omit_delta
              if rec['datamodelDelta'].is_a?(Hash)
                dm = rec['datamodelDelta']
                rec['datamodelDelta'] = dm.keys.sort.each_with_object({}) { |k, h| h[k] = dm[k] }
              end
            else
              rec['datamodelDelta'] = {}
            end
            unless keep_cond
              if rec['firedTransitions'].is_a?(Array)
                rec['firedTransitions'] = rec['firedTransitions'].map do |ft|
                  if ft.is_a?(Hash)
                    ft['cond'] = nil
                  end
                  ft
                end
              end
            end
            rec['firedTransitions'] = [] if omit_transitions
            sink.write(JSON.generate({ step: step_no }.merge(rec)) + "\n")
            step_no += 1
        rescue StandardError
          # ignore malformed
        end
        next
      end
      break if max_steps && step_no > max_steps
      evt_name = (msg.is_a?(Hash) && (msg['event'] || msg['name']))
      next unless evt_name
      evt_data = msg.is_a?(Hash) ? msg['data'] : nil
      rec = ctx.trace_step(name: evt_name.to_s, data: evt_data)
      if leaf_only && leaves
        %w[configuration enteredStates exitedStates].each do |k|
          rec[k] = (rec[k] || []).select { |sid| leaves.include?(sid) }
        end
      end
      rec['actionLog'] = [] if omit_actions
      # sort datamodelDelta keys for deterministic output
      unless omit_delta
        if rec['datamodelDelta'].is_a?(Hash)
          dm = rec['datamodelDelta']
          rec['datamodelDelta'] = dm.keys.sort.each_with_object({}) { |k, h| h[k] = dm[k] }
        end
      else
        rec['datamodelDelta'] = {}
      end
      # scrub cond in firedTransitions unless requested
      unless keep_cond
        if rec['firedTransitions'].is_a?(Array)
          rec['firedTransitions'] = rec['firedTransitions'].map do |ft|
            if ft.is_a?(Hash)
              ft['cond'] = nil
            end
            ft
          end
        end
      end
      rec['firedTransitions'] = [] if omit_transitions
      sink.write(JSON.generate({ step: step_no }.merge(rec)) + "\n")
      step_no += 1
    end
  ensure
    sink.close if sink && sink != $stdout
  end
end