ctx_runner 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb856e1dde339c23607f185f8ac5ac0ab9bca29d94dc115661b145d1f2dfef5a
4
- data.tar.gz: a255c77dafecab0b028974ebf2173efa5c412771621454a7e5c7cf2f6f0ee1cf
3
+ metadata.gz: e38041886eddd4c62f64ac92bb2af4e5f82f62accfe84d276cf169463b1bfd5e
4
+ data.tar.gz: 57174343df0368bf45997665c436781585d23a165a496de58b2ae2f7a7c0c612
5
5
  SHA512:
6
- metadata.gz: b2bfdd9ed85c4c2f48dfcaa568575a39f38cd6d7ebb952f198ed0d251b62b8749fe519de9886162bdaffa907733b4d12259bcf85d6b79a1aec7fa10868b1d8d7
7
- data.tar.gz: 1e0fd95eddbe30863f53c3ac854d0c4a6452b17ad5b42e91c60b9dd4789ff162d7625ffb8a96c9b92e09bb1aae1d5b812600206c922fcf683f146796f9920c46
6
+ metadata.gz: 79b6a398ac323338de8656129163d6863761ee568221e55f57e465b2e14fb161a3c6bc7eebf6dc1895ba8a80aea113d041b68e576e7611e85ce6ccce66f4ccb6
7
+ data.tar.gz: 83f2e533cc140782d46985cd1084d6a729a68d632718edc4ab86b4bce672a47bf2242627b24b03363a9267a76de182d3bee6654ea08362fac4eebd6fbdeb3d21
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0 (2026-10-23)
6
+
7
+ - Breaking: report presenters, completion callbacks and progress callbacks now receive immutable string-keyed documents; remove Report#serializable and migrate consumers.
8
+ - Validate summary and execution documents before publication, remove stale completion after conversion failures, and confirm recovery checkpoint durability.
9
+ - Preserve collected evidence and the original exit when final progress abandons a batch without replaying progress.
10
+ - Use English evidence validation and report publication diagnostics.
11
+
5
12
  ## 0.1.1 (2026-10-23)
6
13
 
7
14
  - Add shared offline evidence validation with mandatory target identity.
data/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  | `Observation` | 事实外壳、可用性、时间类型、深度快照及安全展示 |
12
12
  | `Batch` | 单设备结果登记、报告交付、输出失败补写 |
13
13
  | `Report` | 私有批次目录、文件发布、检查点、完成判定及恢复 |
14
+ | `Document` | 有界、无歧义、深冻结的 JSON 值;统一报告和进度交付表示 |
14
15
  | `Fingerprint` | 稳定配置身份,拒绝归一化键冲突 |
15
16
  | `Execution` | 已确认增量的前提核验、执行记账、效果不确定状态及回读验收 |
16
17
 
@@ -42,6 +43,12 @@ end
42
43
 
43
44
  `Report.new(root)` 创建排他的私有批次,目录 0700、文件 0600。`finish(results, requested:) { |summary| text }` 先保存未完成检查点,再发布领域文本,最后发布最终 JSON。`Report::Error#committed?` 区分是否已替换;失败不能被当作可靠发布。`recovery_failed?` 提示恢复也未确认。
44
45
 
46
+ `finish` 的展示块、返回摘要与最终 JSON 使用相同的字符串键/状态文档,例如 `summary.fetch("status") == "complete"`;展示回调不能改变这份深冻结快照。`Batch::Completion#status` 仍是符号,`#summary` 是该文档。`progress.result` 与 `progress.finish` 同样接收字符串键文档;`Batch#collect` 的返回值及领域 `report.write` 输入保持 Ruby 设备结果结构。这些交付边界没有符号键兼容分支,旧的 `Report#serializable` 已移除。
47
+
48
+ 最终进度输出发生取消或非局部退出时,Batch 仅补写中断报告,保留已经采集和保存的事实及原退出值,不再调用进度展示或重采设备。普通进度异常记录为输出错误;恢复中的存储异常不会替换最先观察到的退出。
49
+
50
+ `Document.call(value)` 复制键和值,支持普通 Hash/Array、Time 的 ISO-8601 字符串和 Data 记录,拒绝正规化后的同名键、循环、不透明对象、非有限数以及超过 128 层的嵌套。`checkpoint(relative, value)`、`finalize(relative, value, checkpoint:)` 和 `write_execution(state)` 使用同一转换规则。检查点必须包含非空且不为 `complete` 的状态;最终提交失败只能恢复未完成事实。转换、编码或取消失败也会撤销旧完成结论;撤销文件后的目录同步失败会设置 `recovery_failed?`,原失败仍交还调用者。
51
+
45
52
  `Execution` 只接受宿主已经校验、向用户展示且明确确认的计划。领域 change 实现 `commands`、`eligible?(before)`、`satisfied?(after, before:)`;journal 实现 `write_execution(state)`。每次回调获得快照。发送前保存 pending,发送后保留 acknowledged;失败后的 uncertain 禁止自动重放。`run { |operation| connector.open(&operation) }` 在连接器正常退出后才发布 configuration_verified;这不表示业务健康已验收。
46
53
 
47
54
  完整可运行离线例子见 [examples](examples)。
data/examples/report.rb CHANGED
@@ -9,8 +9,8 @@ Dir.mktmpdir do |root|
9
9
  report.publish("evidence.json", JSON.generate(source: "synthetic", records: []))
10
10
  device = { "host" => "192.0.2.1", "name" => "example" }
11
11
  summary = report.finish([ { device: device, status: :complete, report_saved: true } ], requested: [ device ]) do |value|
12
- "Evidence: #{value.fetch(:status)}; health was not evaluated.\n"
12
+ "Evidence: #{value.fetch('status')}; health was not evaluated.\n"
13
13
  end
14
- raise "Report incomplete" unless summary[:status] == :complete
14
+ raise "Report incomplete" unless summary.fetch("status") == "complete"
15
15
  raise "Missing evidence" unless File.file?(File.join(report.directory, "evidence.json"))
16
16
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "ctx_parser/immutable"
4
4
  require "ctx_driver/failure"
5
+ require_relative "document"
5
6
 
6
7
  module CtxRunner
7
8
  # 批量采集的交付账本;调度器负责并发与回收,领域块只返回设备快照。
@@ -53,7 +54,7 @@ module CtxRunner
53
54
  elsif failure || finalization_error
54
55
  :failed
55
56
  else
56
- summary[:status] == :complete ? :complete : :incomplete
57
+ summary.fetch("status") == "complete" ? :complete : :incomplete
57
58
  end
58
59
  completion = Completion.new(status: status, summary: summary, failure: failure, finalization_error: finalization_error,
59
60
  recovery_failed: @report.respond_to?(:recovery_failed?) && @report.recovery_failed?)
@@ -99,7 +100,7 @@ module CtxRunner
99
100
  @report.write(CtxParser::Immutable.copy(result), snapshot: snapshot)
100
101
  @lock.synchronize { result[:report_saved] = true }
101
102
  saved = CtxParser::Immutable.copy(result)
102
- @progress.result(saved)
103
+ @progress.result(Document.call(saved))
103
104
  saved
104
105
  end
105
106
 
@@ -112,8 +113,8 @@ module CtxRunner
112
113
  snapshot = results
113
114
  options = { interrupted: interrupted, requested: @requested, report_error: report_error, execution_error: execution_error }
114
115
  summary = @report.finish(snapshot, **options, output_error: @progress.output_error)
115
- @progress.finish(summary)
116
- if @progress.output_error && !summary[:output_error]
116
+ finish_progress(summary, snapshot, options)
117
+ if @progress.output_error && !summary["output_error"]
117
118
  summary = @report.finish(snapshot, **options, output_error: @progress.output_error)
118
119
  end
119
120
  summary
@@ -123,12 +124,32 @@ module CtxRunner
123
124
 
124
125
  private
125
126
 
127
+ # 终端收尾退出时只补写报告;保留已完成事实及原退出,不重放进度或采集。
128
+ def finish_progress(summary, snapshot, options)
129
+ recovery = { interrupted: true, output_error: summary["output_error"] }
130
+ @progress.finish(summary)
131
+ completed = true
132
+ rescue StandardError, LoadError => error
133
+ recovery = { output_error: { error_type: error.class.name } }
134
+ raise
135
+ ensure
136
+ unless completed
137
+ begin
138
+ @report.finish(snapshot, **options, **recovery)
139
+ rescue StandardError, LoadError, Interrupt
140
+ nil
141
+ end
142
+ end
143
+ end
144
+
126
145
  def settle(options)
127
146
  [ CtxParser::Immutable.copy(finish(**options)), nil ]
128
147
  rescue Interrupt
129
148
  options[:interrupted] = true
130
149
  begin
131
- [ CtxParser::Immutable.copy(finish(**options)), nil ]
150
+ # 取消后的终端不可再次调用;仅确认中断报告的交付状态。
151
+ summary = @report.finish(results, **options, requested: @requested, output_error: @progress.output_error)
152
+ [ CtxParser::Immutable.copy(summary), nil ]
132
153
  rescue StandardError, LoadError, Interrupt => error
133
154
  [ nil, diagnostic(error) ]
134
155
  end
@@ -5,12 +5,14 @@ require "time"
5
5
  module CtxRunner
6
6
  # 显式证据边界:生成独立 JSON 值,不通过文本往返转换业务事实。
7
7
  module Document
8
+ MAX_DEPTH = 128
9
+
8
10
  def self.call(value)
9
11
  convert(value, {}.compare_by_identity, 0)
10
12
  end
11
13
 
12
14
  def self.convert(value, active, depth)
13
- raise ArgumentError, "Document nesting exceeds the evidence budget" if depth > 128
15
+ raise ArgumentError, "Document nesting exceeds the evidence budget" if depth > MAX_DEPTH
14
16
  case value
15
17
  when Hash, Array, Data
16
18
  raise ArgumentError, "Document must not contain cyclic data" if active[value]
@@ -21,10 +23,12 @@ module CtxRunner
21
23
  when Array then value.map { |item| convert(item, active, depth + 1) }.freeze
22
24
  when Hash
23
25
  value.each_with_object({}) do |(key, item), result|
24
- unless [ String, Symbol, Integer, Float, Rational, Complex, NilClass, TrueClass, FalseClass ].any? { |type| key.is_a?(type) }
25
- raise ArgumentError, "Document keys must be plain values"
26
+ # 符号名已由 Ruby 持有并冻结;其他键复制后才冻结,不占用调用方对象。
27
+ name = case key
28
+ when Symbol then key.name
29
+ when String, Integer, Float, Rational, Complex, NilClass, TrueClass, FalseClass then String.new(key.to_s)
30
+ else raise ArgumentError, "Document keys must be plain values"
26
31
  end
27
- name = String.new(key.to_s)
28
32
  raise ArgumentError, "Document keys collide after normalization" if result.key?(name)
29
33
  result[name.freeze] = convert(item, active, depth + 1)
30
34
  end.freeze
@@ -34,7 +38,8 @@ module CtxRunner
34
38
  end
35
39
  when Time then value.iso8601(6).freeze
36
40
  when String then value.dup.freeze
37
- when Symbol, Rational, Complex then value.to_s.freeze
41
+ when Symbol then value.name
42
+ when Rational, Complex then value.to_s.freeze
38
43
  when Float
39
44
  raise ArgumentError, "Document numbers must be finite" unless value.finite?
40
45
  value
@@ -8,19 +8,19 @@ module CtxRunner
8
8
  def self.validate(document)
9
9
  unless document.is_a?(Hash) && document["device"].is_a?(Hash) &&
10
10
  %w[name host].all? { |key| document["device"][key].is_a?(String) && !document["device"][key].empty? }
11
- raise ArgumentError, "无设备身份"
11
+ raise ArgumentError, "Device identity is missing"
12
12
  end
13
13
  host = document["device"]["host"]
14
- raise ArgumentError, "目标地址须为规范主机地址" unless !host.include?("/") && IPAddr.new(host).to_s == host
14
+ raise ArgumentError, "Target address must be a canonical host address" unless !host.include?("/") && IPAddr.new(host).to_s == host
15
15
  snapshot = document.fetch("snapshot")
16
16
  return if snapshot.nil?
17
- raise ArgumentError, "快照结构错误" unless snapshot.is_a?(Hash) && snapshot["dimensions"].is_a?(Hash)
17
+ raise ArgumentError, "Invalid snapshot structure" unless snapshot.is_a?(Hash) && snapshot["dimensions"].is_a?(Hash)
18
18
  if !snapshot["target"].is_a?(Hash) || snapshot["target"]["host"] != host
19
- raise ArgumentError, "设备身份与采集目标不一致"
19
+ raise ArgumentError, "Device identity does not match the collection target"
20
20
  end
21
21
  snapshot["dimensions"].each do |dimension, observation|
22
22
  unless observation.is_a?(Hash) && %w[complete partial failed unsupported not_requested].include?(observation["status"])
23
- raise ArgumentError, "观测结构错误"
23
+ raise ArgumentError, "Invalid observation structure"
24
24
  end
25
25
  records = array(observation.fetch("records"))
26
26
  array(observation.fetch("issues", []))
@@ -29,12 +29,12 @@ module CtxRunner
29
29
  end
30
30
 
31
31
  def self.array(value)
32
- raise ArgumentError, "列表结构错误" unless value.is_a?(Array) && value.all? { |entry| entry.is_a?(Hash) }
32
+ raise ArgumentError, "Expected a list of records" unless value.is_a?(Array) && value.all? { |entry| entry.is_a?(Hash) }
33
33
  value
34
34
  end
35
35
 
36
36
  def self.map(value)
37
- raise ArgumentError, "映射结构错误" unless value.is_a?(Hash) && value.values.all? { |entry| entry.is_a?(Hash) }
37
+ raise ArgumentError, "Expected a mapping of records" unless value.is_a?(Hash) && value.values.all? { |entry| entry.is_a?(Hash) }
38
38
  value
39
39
  end
40
40
  end
@@ -4,7 +4,7 @@ require "json"
4
4
  require "time"
5
5
  require "tempfile"
6
6
  require "fileutils"
7
- require "ctx_parser/immutable"
7
+ require_relative "document"
8
8
 
9
9
  module CtxRunner
10
10
  # 持有私有证据批次,统一检查点、发布和失败恢复;领域只提供展示内容。
@@ -18,7 +18,7 @@ module CtxRunner
18
18
  # 保留安全的发布阶段与提交状态,不携带底层异常正文。
19
19
  def initialize(relative:, phase:, committed:)
20
20
  @relative, @phase, @committed = relative.dup.freeze, phase, committed
21
- super("报告写入失败:#{relative};阶段 #{phase};#{committed ? '已替换,收尾未确认' : '尚未替换'}")
21
+ super("Report publication failed: #{relative}; phase #{phase}; #{committed ? 'replaced; finalization unconfirmed' : 'not replaced'}")
22
22
  end
23
23
 
24
24
  # 区分原子替换已完成与目录同步尚未确认。
@@ -33,7 +33,7 @@ module CtxRunner
33
33
  prepare_directory(root)
34
34
  stat = File.lstat(root)
35
35
  unless stat.directory? && (stat.mode & 0o077).zero?
36
- raise ArgumentError, "报告根目录须为真实私有目录,权限0700;请设置独立目录并检查 chmod 0700"
36
+ raise ArgumentError, "Report root must be a real private directory with mode 0700; use a dedicated directory and check chmod 0700"
37
37
  end
38
38
  stem = Time.now.utc.strftime("%Y-%m-%d_%H-%M-%S")
39
39
  sequence = 0
@@ -51,46 +51,55 @@ module CtxRunner
51
51
  def finish(results, interrupted: false, requested: [], report_error: nil, execution_error: nil, output_error: nil, review: nil)
52
52
  raise ArgumentError, "Report requires a presentation block" unless block_given?
53
53
  @recovery_failed = false
54
- # 按地址索引一次,避免大批次逐个扫描所有已完成结果。
55
- completed_hosts = results.to_h { |result| [ result.fetch(:device).fetch("host"), true ] }
56
- pending = requested.reject { |device| completed_hosts.key?(device.fetch("host")) }
57
- complete = !report_error && !execution_error && !output_error && pending.empty? && results.any? && results.all? { |r| r[:status] == :complete && r[:report_saved] != false }
58
- summary = { status: interrupted ? :interrupted : (complete ? :complete : :incomplete),
59
- total: [ requested.size, results.size ].max, completed: results.size, pending: pending, counts: results.map { |r| r[:status] }.tally, devices: results,
60
- directory: directory, evidence_only: true }
61
- summary[:review] = review if review
62
- summary[:report_error] = report_error if report_error
63
- summary[:execution_error] = execution_error if execution_error
64
- summary[:output_error] = output_error if output_error
65
- summary = CtxParser::Immutable.copy(serializable(summary))
66
- # 先保存未完成检查点;文本发布或最终提交失败都不能留下成功摘要。
67
- checkpoint = summary.merge(status: interrupted ? :interrupted : :incomplete,
68
- report_error: report_error || :report_finalization_unconfirmed)
69
- checkpoint_body = JSON.pretty_generate(checkpoint) + "\n"
70
- publish_checkpoint("summary.json", checkpoint_body)
54
+ summary = checkpoint = nil
55
+ # 转换和编码也属于发布;失败或取消不能让上一轮完成结论继续生效。
56
+ checkpoint_body = publish_checkpoint("summary.json") do
57
+ # 按地址索引一次,避免大批次逐个扫描所有已完成结果。
58
+ completed_hosts = results.to_h { |result| [ result.fetch(:device).fetch("host"), true ] }
59
+ pending = requested.reject { |device| completed_hosts.key?(device.fetch("host")) }
60
+ complete = !report_error && !execution_error && !output_error && pending.empty? && results.any? && results.all? { |r| r[:status] == :complete && r[:report_saved] != false }
61
+ summary = { status: interrupted ? :interrupted : (complete ? :complete : :incomplete),
62
+ total: [ requested.size, results.size ].max, completed: results.size, pending: pending, counts: results.map { |r| r[:status] }.tally, devices: results,
63
+ directory: directory, evidence_only: true }
64
+ summary[:review] = review if review
65
+ summary[:report_error] = report_error if report_error
66
+ summary[:execution_error] = execution_error if execution_error
67
+ summary[:output_error] = output_error if output_error
68
+ summary = Document.call(summary)
69
+ # 检查点共享已固定的证据;不重复转换,也不重新读取调用方对象。
70
+ checkpoint = summary.merge("status" => interrupted ? "interrupted" : "incomplete",
71
+ "report_error" => summary["report_error"] || "report_finalization_unconfirmed").freeze
72
+ document_body(checkpoint)
73
+ end
71
74
  publish("summary.txt", yield(summary))
72
75
  commit_summary("summary.json", summary, checkpoint: checkpoint, checkpoint_body: checkpoint_body)
73
76
  end
74
77
 
75
78
  # 新一轮发布必须先撤销旧完成结论;非局部退出也不能绕过这条边界。
76
79
  def checkpoint(relative, value)
77
- publish_checkpoint(relative, JSON.pretty_generate(serializable(value)) + "\n")
80
+ publish_checkpoint(relative) { document_body(checkpoint_document(value)) }
78
81
  end
79
82
 
80
83
  # 领域报告正文完成后提交最终状态;取消和提交后失败共用恢复协议。
81
84
  def finalize(relative, summary, checkpoint:)
82
85
  relative = checked_relative(relative)
83
- summary = CtxParser::Immutable.copy(serializable(summary))
84
- checkpoint = CtxParser::Immutable.copy(serializable(checkpoint))
85
- commit_summary(relative, summary, checkpoint: checkpoint, checkpoint_body: JSON.pretty_generate(checkpoint) + "\n")
86
+ validated = true
87
+ summary = Document.call(summary)
88
+ checkpoint = checkpoint_document(checkpoint)
89
+ commit_summary(relative, summary, checkpoint: checkpoint, checkpoint_body: document_body(checkpoint))
90
+ finalized = true
91
+ summary
92
+ ensure
93
+ invalidate_completed_summary(relative: relative) if validated && !finalized
86
94
  end
87
95
 
88
- def publish_checkpoint(relative, body)
96
+ def publish_checkpoint(relative)
89
97
  relative = checked_relative(relative)
90
98
  validated = true
91
- published = false
99
+ body = yield
92
100
  publish(relative, body)
93
101
  published = true
102
+ body
94
103
  ensure
95
104
  invalidate_completed_summary(relative: relative) if validated && !published
96
105
  end
@@ -99,7 +108,7 @@ module CtxRunner
99
108
  def commit_summary(relative, summary, checkpoint:, checkpoint_body:)
100
109
  finalized = false
101
110
  begin
102
- publish(relative, JSON.pretty_generate(summary) + "\n")
111
+ publish(relative, document_body(summary))
103
112
  finalized = true
104
113
  rescue Error => error
105
114
  # 未替换时原检查点仍安全,不做可能破坏它的多余恢复写入。
@@ -107,7 +116,7 @@ module CtxRunner
107
116
  finalized = true
108
117
  raise
109
118
  rescue Interrupt
110
- restore_checkpoint(JSON.pretty_generate(checkpoint.merge(status: :interrupted)) + "\n", relative: relative)
119
+ restore_checkpoint(document_body(checkpoint.merge("status" => "interrupted")), relative: relative)
111
120
  finalized = true
112
121
  raise
113
122
  ensure
@@ -120,7 +129,7 @@ module CtxRunner
120
129
 
121
130
  # 每次发送配置前后发布检查点;失败由执行器停止后续写入。
122
131
  def write_execution(state)
123
- publish("execution.json", JSON.pretty_generate(state) + "\n")
132
+ publish("execution.json", document_body(Document.call(state)))
124
133
  end
125
134
 
126
135
  # 恢复写入或删除受阻时,调用方须把退出码及此状态作为未确认诊断。
@@ -149,7 +158,7 @@ module CtxRunner
149
158
  def invalidate_completed_summary(relative: "summary.json")
150
159
  relative = checked_relative(relative)
151
160
  Thread.handle_interrupt(Exception => :never) do
152
- saved = JSON.parse(File.read(File.join(directory, relative)))
161
+ saved = JSON.parse(File.read(File.join(directory, relative)), max_nesting: Document::MAX_DEPTH + 1)
153
162
  discard_summary(relative: relative) if saved["status"] == "complete"
154
163
  end
155
164
  rescue Errno::ENOENT
@@ -176,24 +185,15 @@ module CtxRunner
176
185
  # 存储拒绝恢复时尽力移除不可信摘要,不删除已获得的设备事实。
177
186
  def discard_summary(relative: "summary.json")
178
187
  relative = checked_relative(relative)
179
- File.unlink(File.join(directory, relative))
188
+ target = File.join(directory, relative)
189
+ File.unlink(target)
190
+ sync_directory(File.dirname(target))
180
191
  rescue Errno::ENOENT
181
192
  nil
182
- rescue SystemCallError
193
+ rescue SystemCallError, IOError, NotImplementedError
183
194
  @recovery_failed = true
184
195
  end
185
196
 
186
- # Time 使用 ISO-8601,递归转换 Data 只发生在显式持久化边界。
187
- def serializable(value)
188
- case value
189
- when Time then value.iso8601(6)
190
- when Data then serializable(value.to_h)
191
- when Hash then value.transform_values { |item| serializable(item) }
192
- when Array then value.map { |item| serializable(item) }
193
- else value
194
- end
195
- end
196
-
197
197
  # 临时文件与目标在同一目录,写入失败不发布半份报告。
198
198
  def publish(relative, body)
199
199
  relative = checked_relative(relative)
@@ -218,6 +218,18 @@ module CtxRunner
218
218
  end
219
219
  private
220
220
 
221
+ # 恢复只能保留未完成事实,不能把完成摘要当作安全检查点。
222
+ def checkpoint_document(value)
223
+ document = Document.call(value)
224
+ unless document.is_a?(Hash) && document["status"].is_a?(String) && !document["status"].empty? && document["status"] != "complete"
225
+ raise ArgumentError, "Checkpoint requires a non-complete status"
226
+ end
227
+ document
228
+ end
229
+
230
+ # 深度由 Document 校验;编码阶段不再施加 JSON 默认的更低嵌套限制。
231
+ def document_body(document) = JSON.pretty_generate(document, max_nesting: false) + "\n"
232
+
221
233
  # 发布、恢复与删除使用同一条路径契约;拒绝路径时不能执行收尾副作用。
222
234
  def checked_relative(relative)
223
235
  unless relative.is_a?(String) && !relative.empty? && !relative.start_with?("/") &&
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CtxRunner
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ctx_runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -13,42 +13,30 @@ dependencies:
13
13
  name: ctx_parser
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '0.2'
19
16
  - - ">="
20
17
  - !ruby/object:Gem::Version
21
- version: 0.2.6
18
+ version: 0.2.7
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
- - - "~>"
27
- - !ruby/object:Gem::Version
28
- version: '0.2'
29
23
  - - ">="
30
24
  - !ruby/object:Gem::Version
31
- version: 0.2.6
25
+ version: 0.2.7
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: ctx_driver
34
28
  requirement: !ruby/object:Gem::Requirement
35
29
  requirements:
36
- - - "~>"
37
- - !ruby/object:Gem::Version
38
- version: '0.2'
39
30
  - - ">="
40
31
  - !ruby/object:Gem::Version
41
- version: 0.2.6
32
+ version: 0.2.7
42
33
  type: :runtime
43
34
  prerelease: false
44
35
  version_requirements: !ruby/object:Gem::Requirement
45
36
  requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '0.2'
49
37
  - - ">="
50
38
  - !ruby/object:Gem::Version
51
- version: 0.2.6
39
+ version: 0.2.7
52
40
  - !ruby/object:Gem::Dependency
53
41
  name: digest
54
42
  requirement: !ruby/object:Gem::Requirement