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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +7 -0
- data/examples/report.rb +2 -2
- data/lib/ctx_runner/batch.rb +26 -5
- data/lib/ctx_runner/document.rb +10 -5
- data/lib/ctx_runner/evidence.rb +7 -7
- data/lib/ctx_runner/report.rb +55 -43
- data/lib/ctx_runner/version.rb +1 -1
- metadata +5 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e38041886eddd4c62f64ac92bb2af4e5f82f62accfe84d276cf169463b1bfd5e
|
|
4
|
+
data.tar.gz: 57174343df0368bf45997665c436781585d23a165a496de58b2ae2f7a7c0c612
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
12
|
+
"Evidence: #{value.fetch('status')}; health was not evaluated.\n"
|
|
13
13
|
end
|
|
14
|
-
raise "Report incomplete" unless summary
|
|
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
|
data/lib/ctx_runner/batch.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
116
|
-
if @progress.output_error && !summary[
|
|
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
|
-
|
|
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
|
data/lib/ctx_runner/document.rb
CHANGED
|
@@ -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 >
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
data/lib/ctx_runner/evidence.rb
CHANGED
|
@@ -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, "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
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
|
data/lib/ctx_runner/report.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
require "time"
|
|
5
5
|
require "tempfile"
|
|
6
6
|
require "fileutils"
|
|
7
|
-
|
|
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("
|
|
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, "
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
96
|
+
def publish_checkpoint(relative)
|
|
89
97
|
relative = checked_relative(relative)
|
|
90
98
|
validated = true
|
|
91
|
-
|
|
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,
|
|
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(
|
|
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",
|
|
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.
|
|
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?("/") &&
|
data/lib/ctx_runner/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
+
version: 0.2.7
|
|
52
40
|
- !ruby/object:Gem::Dependency
|
|
53
41
|
name: digest
|
|
54
42
|
requirement: !ruby/object:Gem::Requirement
|