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.
- checksums.yaml +4 -4
- data/LEGAL.md +5 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/lib/scjson/cli.rb +86 -2
- data/lib/scjson/engine/context.rb +1597 -0
- data/lib/scjson/engine.rb +187 -0
- data/lib/scjson/types.rb +1964 -0
- data/lib/scjson/version.rb +1 -1
- data/lib/scjson.rb +76 -16
- metadata +19 -6
|
@@ -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
|