textus 0.47.1 → 0.50.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 +51 -0
- data/README.md +9 -7
- data/SPEC.md +44 -69
- data/docs/reference/conventions.md +13 -12
- data/lib/textus/boot.rb +47 -32
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/cli/runner.rb +5 -4
- data/lib/textus/cli/verb/boot.rb +2 -1
- data/lib/textus/cli.rb +0 -1
- data/lib/textus/dispatcher.rb +3 -8
- data/lib/textus/doctor/check/generator_drift.rb +28 -0
- data/lib/textus/doctor/check/lifecycle_action_invalid.rb +39 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +1 -1
- data/lib/textus/doctor.rb +2 -0
- data/lib/textus/domain/lifecycle.rb +83 -0
- data/lib/textus/domain/policy/base_guards.rb +2 -2
- data/lib/textus/domain/policy/lifecycle.rb +35 -0
- data/lib/textus/domain/staleness.rb +6 -3
- data/lib/textus/envelope/io/writer.rb +2 -2
- data/lib/textus/hooks/context.rb +1 -1
- data/lib/textus/init.rb +4 -4
- data/lib/textus/maintenance/key_delete_prefix.rb +1 -1
- data/lib/textus/maintenance/key_mv_prefix.rb +2 -2
- data/lib/textus/maintenance/tend.rb +110 -0
- data/lib/textus/manifest/entry/base.rb +1 -0
- data/lib/textus/manifest/entry/derived.rb +4 -2
- data/lib/textus/manifest/rules.rb +11 -23
- data/lib/textus/manifest/schema.rb +4 -19
- data/lib/textus/mcp/server.rb +9 -2
- data/lib/textus/ports/audit_log.rb +1 -1
- data/lib/textus/ports/audit_subscriber.rb +1 -1
- data/lib/textus/ports/fetch/detached.rb +5 -1
- data/lib/textus/read/boot.rb +4 -2
- data/lib/textus/read/freshness.rb +37 -26
- data/lib/textus/read/get.rb +47 -32
- data/lib/textus/read/pulse.rb +1 -1
- data/lib/textus/read/rule_explain.rb +10 -16
- data/lib/textus/read/rule_list.rb +5 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +1 -1
- data/lib/textus/write/fetch_worker.rb +8 -12
- data/lib/textus/write/{delete.rb → key_delete.rb} +3 -3
- data/lib/textus/write/{mv.rb → key_mv.rb} +4 -4
- data/lib/textus/write/reject.rb +1 -1
- metadata +8 -15
- data/lib/textus/cli/group/fetch.rb +0 -20
- data/lib/textus/cli/verb/fetch.rb +0 -14
- data/lib/textus/cli/verb/fetch_all.rb +0 -20
- data/lib/textus/domain/policy/fetch.rb +0 -37
- data/lib/textus/domain/policy/retention.rb +0 -26
- data/lib/textus/domain/retention.rb +0 -44
- data/lib/textus/domain/staleness/intake_check.rb +0 -54
- data/lib/textus/maintenance/migrate.rb +0 -65
- data/lib/textus/read/retainable.rb +0 -17
- data/lib/textus/read/stale.rb +0 -17
- data/lib/textus/write/fetch_all.rb +0 -53
- data/lib/textus/write/retention_sweep.rb +0 -64
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
|
-
class
|
|
3
|
+
class KeyMv
|
|
4
4
|
extend Textus::Contract::DSL
|
|
5
5
|
|
|
6
|
-
verb :
|
|
6
|
+
verb :key_mv
|
|
7
7
|
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
8
8
|
surfaces :cli, :mcp
|
|
9
9
|
cli "key mv"
|
|
@@ -53,8 +53,8 @@ module Textus
|
|
|
53
53
|
raise UnknownKey.new(old_key) unless reader.exists?(old_key)
|
|
54
54
|
|
|
55
55
|
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
56
|
-
guard_for(:
|
|
57
|
-
guard_for(:
|
|
56
|
+
guard_for(:key_mv, old_key).check!(eval_for(:key_mv, target_key: old_key))
|
|
57
|
+
guard_for(:key_mv, new_key).check!(eval_for(:key_mv, target_key: new_key))
|
|
58
58
|
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
|
|
59
59
|
|
|
60
60
|
[old_res, new_res]
|
data/lib/textus/write/reject.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.50.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -120,7 +120,6 @@ files:
|
|
|
120
120
|
- lib/textus/call.rb
|
|
121
121
|
- lib/textus/cli.rb
|
|
122
122
|
- lib/textus/cli/group.rb
|
|
123
|
-
- lib/textus/cli/group/fetch.rb
|
|
124
123
|
- lib/textus/cli/group/hook.rb
|
|
125
124
|
- lib/textus/cli/group/key.rb
|
|
126
125
|
- lib/textus/cli/group/mcp.rb
|
|
@@ -132,8 +131,6 @@ files:
|
|
|
132
131
|
- lib/textus/cli/verb/boot.rb
|
|
133
132
|
- lib/textus/cli/verb/build.rb
|
|
134
133
|
- lib/textus/cli/verb/doctor.rb
|
|
135
|
-
- lib/textus/cli/verb/fetch.rb
|
|
136
|
-
- lib/textus/cli/verb/fetch_all.rb
|
|
137
134
|
- lib/textus/cli/verb/get.rb
|
|
138
135
|
- lib/textus/cli/verb/hook_run.rb
|
|
139
136
|
- lib/textus/cli/verb/hooks.rb
|
|
@@ -157,10 +154,12 @@ files:
|
|
|
157
154
|
- lib/textus/doctor/check.rb
|
|
158
155
|
- lib/textus/doctor/check/audit_log.rb
|
|
159
156
|
- lib/textus/doctor/check/fetch_locks.rb
|
|
157
|
+
- lib/textus/doctor/check/generator_drift.rb
|
|
160
158
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
161
159
|
- lib/textus/doctor/check/hooks.rb
|
|
162
160
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
163
161
|
- lib/textus/doctor/check/intake_registration.rb
|
|
162
|
+
- lib/textus/doctor/check/lifecycle_action_invalid.rb
|
|
164
163
|
- lib/textus/doctor/check/manifest_files.rb
|
|
165
164
|
- lib/textus/doctor/check/orphaned_publish_targets.rb
|
|
166
165
|
- lib/textus/doctor/check/proposal_targets.rb
|
|
@@ -179,14 +178,15 @@ files:
|
|
|
179
178
|
- lib/textus/domain/freshness/evaluator.rb
|
|
180
179
|
- lib/textus/domain/freshness/policy.rb
|
|
181
180
|
- lib/textus/domain/freshness/verdict.rb
|
|
181
|
+
- lib/textus/domain/lifecycle.rb
|
|
182
182
|
- lib/textus/domain/outcome.rb
|
|
183
183
|
- lib/textus/domain/permission.rb
|
|
184
184
|
- lib/textus/domain/policy/base_guards.rb
|
|
185
185
|
- lib/textus/domain/policy/evaluation.rb
|
|
186
|
-
- lib/textus/domain/policy/fetch.rb
|
|
187
186
|
- lib/textus/domain/policy/guard.rb
|
|
188
187
|
- lib/textus/domain/policy/guard_factory.rb
|
|
189
188
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
189
|
+
- lib/textus/domain/policy/lifecycle.rb
|
|
190
190
|
- lib/textus/domain/policy/matcher.rb
|
|
191
191
|
- lib/textus/domain/policy/predicates/author_held.rb
|
|
192
192
|
- lib/textus/domain/policy/predicates/etag_match.rb
|
|
@@ -195,12 +195,9 @@ files:
|
|
|
195
195
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
196
196
|
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
197
197
|
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
198
|
-
- lib/textus/domain/policy/retention.rb
|
|
199
|
-
- lib/textus/domain/retention.rb
|
|
200
198
|
- lib/textus/domain/sentinel.rb
|
|
201
199
|
- lib/textus/domain/staleness.rb
|
|
202
200
|
- lib/textus/domain/staleness/generator_check.rb
|
|
203
|
-
- lib/textus/domain/staleness/intake_check.rb
|
|
204
201
|
- lib/textus/entry.rb
|
|
205
202
|
- lib/textus/entry/base.rb
|
|
206
203
|
- lib/textus/entry/json.rb
|
|
@@ -231,8 +228,8 @@ files:
|
|
|
231
228
|
- lib/textus/maintenance.rb
|
|
232
229
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
233
230
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
234
|
-
- lib/textus/maintenance/migrate.rb
|
|
235
231
|
- lib/textus/maintenance/rule_lint.rb
|
|
232
|
+
- lib/textus/maintenance/tend.rb
|
|
236
233
|
- lib/textus/maintenance/zone_mv.rb
|
|
237
234
|
- lib/textus/manifest.rb
|
|
238
235
|
- lib/textus/manifest/capabilities.rb
|
|
@@ -293,11 +290,9 @@ files:
|
|
|
293
290
|
- lib/textus/read/published.rb
|
|
294
291
|
- lib/textus/read/pulse.rb
|
|
295
292
|
- lib/textus/read/rdeps.rb
|
|
296
|
-
- lib/textus/read/retainable.rb
|
|
297
293
|
- lib/textus/read/rule_explain.rb
|
|
298
294
|
- lib/textus/read/rule_list.rb
|
|
299
295
|
- lib/textus/read/schema_envelope.rb
|
|
300
|
-
- lib/textus/read/stale.rb
|
|
301
296
|
- lib/textus/read/uid.rb
|
|
302
297
|
- lib/textus/read/validate_all.rb
|
|
303
298
|
- lib/textus/read/validator.rb
|
|
@@ -313,18 +308,16 @@ files:
|
|
|
313
308
|
- lib/textus/version.rb
|
|
314
309
|
- lib/textus/write/accept.rb
|
|
315
310
|
- lib/textus/write/build.rb
|
|
316
|
-
- lib/textus/write/delete.rb
|
|
317
|
-
- lib/textus/write/fetch_all.rb
|
|
318
311
|
- lib/textus/write/fetch_events.rb
|
|
319
312
|
- lib/textus/write/fetch_orchestrator.rb
|
|
320
313
|
- lib/textus/write/fetch_worker.rb
|
|
321
314
|
- lib/textus/write/intake_fetch.rb
|
|
315
|
+
- lib/textus/write/key_delete.rb
|
|
316
|
+
- lib/textus/write/key_mv.rb
|
|
322
317
|
- lib/textus/write/materializer.rb
|
|
323
|
-
- lib/textus/write/mv.rb
|
|
324
318
|
- lib/textus/write/propose.rb
|
|
325
319
|
- lib/textus/write/put.rb
|
|
326
320
|
- lib/textus/write/reject.rb
|
|
327
|
-
- lib/textus/write/retention_sweep.rb
|
|
328
321
|
homepage: https://github.com/patrick204nqh/textus
|
|
329
322
|
licenses:
|
|
330
323
|
- MIT
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Group
|
|
4
|
-
class Fetch < Group
|
|
5
|
-
command_name "fetch"
|
|
6
|
-
|
|
7
|
-
def parse(argv)
|
|
8
|
-
if argv.first == "all"
|
|
9
|
-
argv.shift
|
|
10
|
-
@sub_klass = Verb::FetchAll
|
|
11
|
-
else
|
|
12
|
-
@sub_klass = Verb::Fetch
|
|
13
|
-
end
|
|
14
|
-
@sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
15
|
-
@sub.parse(argv)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Fetch < Verb
|
|
5
|
-
option :as_flag, "--as=ROLE"
|
|
6
|
-
|
|
7
|
-
def call(store)
|
|
8
|
-
key = positional.shift or raise UsageError.new("fetch requires a key")
|
|
9
|
-
emit(session_for(store).fetch(key).to_h_for_wire)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class FetchAll < Verb
|
|
5
|
-
command_name "all"
|
|
6
|
-
parent_group Group::Fetch
|
|
7
|
-
|
|
8
|
-
option :prefix, "--prefix=KEY"
|
|
9
|
-
option :zone, "--zone=Z"
|
|
10
|
-
option :as_flag, "--as=ROLE"
|
|
11
|
-
|
|
12
|
-
def call(store)
|
|
13
|
-
result = session_for(store).fetch_all(prefix: prefix, zone: zone)
|
|
14
|
-
emit(result)
|
|
15
|
-
result["ok"] ? 0 : 1
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
class Fetch
|
|
5
|
-
ALLOWED_ON_STALE = %i[warn sync timed_sync].freeze
|
|
6
|
-
|
|
7
|
-
attr_reader :ttl, :on_stale, :sync_budget_ms, :fetch_timeout_seconds
|
|
8
|
-
|
|
9
|
-
def initialize(ttl:, on_stale:, sync_budget_ms:, fetch_timeout_seconds: nil)
|
|
10
|
-
on_stale_sym = on_stale.is_a?(Symbol) ? on_stale : on_stale.to_s.to_sym
|
|
11
|
-
unless ALLOWED_ON_STALE.include?(on_stale_sym)
|
|
12
|
-
raise Textus::UsageError.new(
|
|
13
|
-
"on_stale must be one of #{ALLOWED_ON_STALE.join(", ")} (got #{on_stale.inspect})",
|
|
14
|
-
)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
@ttl = ttl
|
|
18
|
-
@on_stale = on_stale_sym
|
|
19
|
-
@sync_budget_ms = sync_budget_ms
|
|
20
|
-
@fetch_timeout_seconds = fetch_timeout_seconds
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def ttl_seconds
|
|
24
|
-
Textus::Domain::Duration.seconds(@ttl)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def to_freshness_policy
|
|
28
|
-
Textus::Domain::Freshness::Policy.new(
|
|
29
|
-
ttl_seconds: ttl_seconds,
|
|
30
|
-
on_stale: @on_stale,
|
|
31
|
-
sync_budget_ms: @sync_budget_ms,
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
# Lifetime policy for queue/quarantine leaves. Both windows are optional
|
|
5
|
-
# durations (see Domain::Duration). `expire_after` deletes; `archive_after`
|
|
6
|
-
# moves the leaf aside. When both are set, expire wins once its (longer)
|
|
7
|
-
# window is exceeded.
|
|
8
|
-
class Retention
|
|
9
|
-
attr_reader :expire_after, :archive_after
|
|
10
|
-
|
|
11
|
-
def initialize(expire_after: nil, archive_after: nil)
|
|
12
|
-
@expire_after = Textus::Domain::Duration.seconds(expire_after)
|
|
13
|
-
@archive_after = Textus::Domain::Duration.seconds(archive_after)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# :expire | :archive | nil for a leaf of the given age (seconds).
|
|
17
|
-
def action_for(age_seconds)
|
|
18
|
-
return :expire if @expire_after && age_seconds > @expire_after
|
|
19
|
-
return :archive if @archive_after && age_seconds > @archive_after
|
|
20
|
-
|
|
21
|
-
nil
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
# Reports leaves whose age (now - file mtime) exceeds a retention window.
|
|
4
|
-
# Each row is { "key", "path", "action" => "expire"|"archive", "age_seconds" }.
|
|
5
|
-
class Retention
|
|
6
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
7
|
-
@manifest = manifest
|
|
8
|
-
@file_stat = file_stat
|
|
9
|
-
@clock = clock
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(prefix: nil, zone: nil)
|
|
13
|
-
@manifest.data.entries
|
|
14
|
-
.select { |m| entry_matches?(m, prefix: prefix, zone: zone) }
|
|
15
|
-
.flat_map { |m| rows_for(m) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def rows_for(mentry)
|
|
21
|
-
policy = @manifest.rules.for(mentry.key).retention
|
|
22
|
-
return [] if policy.nil?
|
|
23
|
-
|
|
24
|
-
@manifest.resolver.enumerate(prefix: mentry.key).filter_map do |row|
|
|
25
|
-
path = row[:path]
|
|
26
|
-
next unless @file_stat.exists?(path)
|
|
27
|
-
|
|
28
|
-
age = (@clock.now - @file_stat.mtime(path)).to_i
|
|
29
|
-
action = policy.action_for(age)
|
|
30
|
-
next if action.nil?
|
|
31
|
-
|
|
32
|
-
{ "key" => row[:key], "path" => path, "action" => action.to_s, "age_seconds" => age }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def entry_matches?(mentry, prefix:, zone:)
|
|
37
|
-
return false if zone && mentry.zone != zone
|
|
38
|
-
return false if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
39
|
-
|
|
40
|
-
true
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
class Staleness
|
|
6
|
-
# Reports TTL-exceeded staleness for intake-handler entries. Returns an
|
|
7
|
-
# Array of row hashes (possibly empty) per entry.
|
|
8
|
-
class IntakeCheck
|
|
9
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
10
|
-
@manifest = manifest
|
|
11
|
-
@file_stat = file_stat
|
|
12
|
-
@clock = clock
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def rows_for(mentry)
|
|
16
|
-
return [] unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
17
|
-
|
|
18
|
-
ttl = @manifest.rules.for(mentry.key).fetch&.ttl_seconds
|
|
19
|
-
return [] unless ttl
|
|
20
|
-
|
|
21
|
-
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
22
|
-
reason = ttl_reason(mentry, path, ttl)
|
|
23
|
-
reason ? [row(mentry, path, reason)] : []
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def ttl_reason(mentry, path, ttl)
|
|
29
|
-
return "never fetched" unless @file_stat.exists?(path)
|
|
30
|
-
|
|
31
|
-
last_str = last_fetched_of(mentry, path)
|
|
32
|
-
return "never fetched (no last_fetched_at)" if last_str.nil?
|
|
33
|
-
|
|
34
|
-
last = parse_time(last_str)
|
|
35
|
-
"ttl exceeded (#{ttl}s)" if last.nil? || (@clock.now - last) > ttl
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def last_fetched_of(mentry, path)
|
|
39
|
-
Entry.for_format(mentry.format).parse(@file_stat.read(path), path: path)["_meta"]["last_fetched_at"]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def parse_time(str)
|
|
43
|
-
Time.parse(str.to_s)
|
|
44
|
-
rescue StandardError
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def row(mentry, path, reason)
|
|
49
|
-
{ "key" => mentry.key, "path" => path, "handler" => mentry.handler, "reason" => reason }
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
# Loads a YAML migration plan and dispatches each op to the
|
|
6
|
-
# appropriate Maintenance use case. Concatenates resulting Plans.
|
|
7
|
-
class Migrate
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :migrate
|
|
11
|
-
summary "Run a YAML migration plan (multi-op)."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
arg :plan_yaml, String, required: true, positional: true, source: :file,
|
|
14
|
-
description: "path to the YAML migration plan (zone_mv, key_mv_prefix, key_delete_prefix ops run in order)"
|
|
15
|
-
arg :dry_run, :boolean, default: false,
|
|
16
|
-
description: "when true, returns the planned ops without applying them; " \
|
|
17
|
-
"defaults to false, so omitting it runs the migration immediately"
|
|
18
|
-
view { |v, _i| v.to_h }
|
|
19
|
-
|
|
20
|
-
def initialize(container:, call:)
|
|
21
|
-
@container = container
|
|
22
|
-
@call = call
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def call(plan_yaml, dry_run: false)
|
|
26
|
-
raw = YAML.safe_load(plan_yaml, permitted_classes: [Symbol], aliases: false)
|
|
27
|
-
raise UsageError.new("migration plan must be a YAML mapping") unless raw.is_a?(Hash)
|
|
28
|
-
|
|
29
|
-
ops = Array(raw["operations"])
|
|
30
|
-
all_steps = []
|
|
31
|
-
warnings = []
|
|
32
|
-
|
|
33
|
-
ops.each do |op_hash|
|
|
34
|
-
op_name = op_hash["op"]
|
|
35
|
-
sub_plan = invoke_op(op_name, op_hash, dry_run: dry_run)
|
|
36
|
-
all_steps.concat(sub_plan.steps)
|
|
37
|
-
warnings.concat(sub_plan.warnings)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
Plan.new(steps: all_steps, warnings: warnings)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def invoke_op(op_name, op_hash, dry_run:)
|
|
46
|
-
klass = op_class(op_name)
|
|
47
|
-
inputs = op_hash.except("op").transform_keys(&:to_sym).merge(dry_run: dry_run)
|
|
48
|
-
# Each op now carries positional args (from/to, from_prefix/to_prefix,
|
|
49
|
-
# prefix); split the YAML fields into (positional, keyword) via the op's
|
|
50
|
-
# own contract so we call its #call signature correctly (ADR 0066/0068).
|
|
51
|
-
args, kwargs = Textus::Contract::Binder.bind(klass.contract, inputs)
|
|
52
|
-
klass.new(container: @container, call: @call).call(*args, **kwargs)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def op_class(op_name)
|
|
56
|
-
case op_name
|
|
57
|
-
when "key_mv_prefix" then KeyMvPrefix
|
|
58
|
-
when "key_delete_prefix" then KeyDeletePrefix
|
|
59
|
-
when "zone_mv" then ZoneMv
|
|
60
|
-
else raise UsageError.new("unknown op: #{op_name}")
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Retainable
|
|
4
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
5
|
-
@manifest = container.manifest
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def call(prefix: nil, zone: nil)
|
|
9
|
-
Textus::Domain::Retention.new(
|
|
10
|
-
manifest: @manifest,
|
|
11
|
-
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
12
|
-
clock: Textus::Ports::Clock,
|
|
13
|
-
).call(prefix: prefix, zone: zone)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/read/stale.rb
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
class Stale
|
|
4
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
5
|
-
@manifest = container.manifest
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def call(prefix: nil, zone: nil)
|
|
9
|
-
Textus::Domain::Staleness.new(
|
|
10
|
-
manifest: @manifest,
|
|
11
|
-
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
12
|
-
clock: Textus::Ports::Clock,
|
|
13
|
-
).call(prefix: prefix, zone: zone)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
class FetchAll
|
|
4
|
-
extend Textus::Contract::DSL
|
|
5
|
-
|
|
6
|
-
verb :fetch_all
|
|
7
|
-
summary "Fetch all stale quarantine entries, optionally scoped by zone/prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "fetch all"
|
|
10
|
-
arg :prefix, String, description: "only refresh stale entries whose key starts with this dotted prefix"
|
|
11
|
-
arg :zone, String, description: "only refresh stale entries in this quarantine zone (see `pulse` stale list)"
|
|
12
|
-
|
|
13
|
-
def initialize(container:, call:)
|
|
14
|
-
@container = container
|
|
15
|
-
@call = call
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(prefix: nil, zone: nil)
|
|
19
|
-
worker = Textus::Write::FetchWorker.new(
|
|
20
|
-
container: @container, call: @call,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
stale_rows = Textus::Read::Stale.new(container: @container, call: @call).call(prefix: prefix, zone: zone)
|
|
24
|
-
fetched = []
|
|
25
|
-
failed = []
|
|
26
|
-
skipped = []
|
|
27
|
-
|
|
28
|
-
stale_rows.each do |row|
|
|
29
|
-
key = row["key"] || row[:key]
|
|
30
|
-
reason = row["reason"] || row[:reason]
|
|
31
|
-
if reason.to_s.match?(/ttl exceeded|never fetched/)
|
|
32
|
-
begin
|
|
33
|
-
worker.run(key)
|
|
34
|
-
fetched << key
|
|
35
|
-
rescue Textus::Error => e
|
|
36
|
-
failed << { "key" => key, "error" => e.message }
|
|
37
|
-
end
|
|
38
|
-
else
|
|
39
|
-
skipped << { "key" => key, "reason" => reason }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
"protocol" => Textus::PROTOCOL,
|
|
45
|
-
"ok" => failed.empty?,
|
|
46
|
-
"fetched" => fetched,
|
|
47
|
-
"failed" => failed,
|
|
48
|
-
"skipped" => skipped,
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Write
|
|
5
|
-
# Applies retention actions reported by Read::Retainable. `expire` deletes
|
|
6
|
-
# the leaf through the role gate; `archive` copies it to
|
|
7
|
-
# <root>/archive/<relative-path> first, then deletes. Rows whose zone the
|
|
8
|
-
# caller's role cannot write surface in `failed` rather than aborting.
|
|
9
|
-
class RetentionSweep
|
|
10
|
-
extend Textus::Contract::DSL
|
|
11
|
-
|
|
12
|
-
verb :retain
|
|
13
|
-
summary "Apply each entry's retention policy; prune expired versions."
|
|
14
|
-
surfaces :cli
|
|
15
|
-
cli "retain"
|
|
16
|
-
arg :prefix, String, description: "restrict to keys starting with this dotted prefix"
|
|
17
|
-
arg :zone, String, description: "restrict to entries in this zone"
|
|
18
|
-
|
|
19
|
-
def initialize(container:, call:)
|
|
20
|
-
@container = container
|
|
21
|
-
@call = call
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def call(prefix: nil, zone: nil)
|
|
25
|
-
rows = Textus::Read::Retainable.new(container: @container, call: @call)
|
|
26
|
-
.call(prefix: prefix, zone: zone)
|
|
27
|
-
delete_op = Textus::Write::Delete.new(container: @container, call: @call)
|
|
28
|
-
expired = []
|
|
29
|
-
archived = []
|
|
30
|
-
failed = []
|
|
31
|
-
|
|
32
|
-
rows.each do |row|
|
|
33
|
-
key = row["key"]
|
|
34
|
-
begin
|
|
35
|
-
archive_leaf(row) if row["action"] == "archive"
|
|
36
|
-
delete_op.call(key)
|
|
37
|
-
(row["action"] == "archive" ? archived : expired) << key
|
|
38
|
-
rescue Textus::Error => e
|
|
39
|
-
failed << { "key" => key, "error" => e.message }
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
"protocol" => Textus::PROTOCOL,
|
|
45
|
-
"ok" => failed.empty?,
|
|
46
|
-
"expired" => expired,
|
|
47
|
-
"archived" => archived,
|
|
48
|
-
"failed" => failed,
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def archive_leaf(row)
|
|
55
|
-
src = row["path"]
|
|
56
|
-
root = @container.root.to_s
|
|
57
|
-
rel = src.delete_prefix("#{root}/")
|
|
58
|
-
dest = File.join(root, "archive", rel)
|
|
59
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
60
|
-
FileUtils.cp(src, dest)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|