textus 0.52.0 → 0.53.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Action
|
|
7
|
+
class DataMv < Base
|
|
8
|
+
extend Textus::Contract::DSL
|
|
9
|
+
|
|
10
|
+
verb :data_mv
|
|
11
|
+
summary "Rename a data lane — manifest + files. Refuses if destination exists."
|
|
12
|
+
surfaces :cli, :mcp
|
|
13
|
+
cli "data mv"
|
|
14
|
+
arg :from, String, required: true, positional: true, description: "current data lane name"
|
|
15
|
+
arg :to, String, required: true, positional: true,
|
|
16
|
+
description: "new data lane name; refused if a lane by this name already exists"
|
|
17
|
+
arg :dry_run, :boolean, default: false,
|
|
18
|
+
description: "when true, returns the planned zone move without applying it; " \
|
|
19
|
+
"defaults to false, so omitting it applies the move immediately"
|
|
20
|
+
view { |v, _i| v.to_h }
|
|
21
|
+
|
|
22
|
+
BURN = :sync
|
|
23
|
+
|
|
24
|
+
def initialize(from:, to:, dry_run: false)
|
|
25
|
+
super()
|
|
26
|
+
@from = from
|
|
27
|
+
@to = to
|
|
28
|
+
@dry_run = dry_run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(container:, **)
|
|
32
|
+
manifest = container.manifest
|
|
33
|
+
root = container.root
|
|
34
|
+
|
|
35
|
+
raise UsageError.new("from and to required") if @from.nil? || @to.nil? || @from.empty? || @to.empty?
|
|
36
|
+
raise UsageError.new("data lane '#{@from}' not declared") unless manifest.data.declared_lane_kinds.key?(@from)
|
|
37
|
+
|
|
38
|
+
dest_dir = File.join(root, "data", @to)
|
|
39
|
+
raise UsageError.new("destination 'data/#{@to}' already exists") if File.exist?(dest_dir)
|
|
40
|
+
|
|
41
|
+
affected_keys = manifest.data.entries.select { |entry| entry.lane == @from }.map(&:key)
|
|
42
|
+
|
|
43
|
+
steps = [{ "op" => "rename_zone", "from" => @from, "to" => @to }]
|
|
44
|
+
steps += affected_keys.map do |key|
|
|
45
|
+
{ "op" => "mv", "from" => key, "to" => "#{@to}#{key[@from.length..]}" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
plan = Textus::Background::Plan.new(steps: steps, warnings: [])
|
|
49
|
+
return plan if @dry_run
|
|
50
|
+
|
|
51
|
+
rewrite_manifest!(root)
|
|
52
|
+
FileUtils.mv(File.join(root, "data", @from), dest_dir)
|
|
53
|
+
plan
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def rewrite_manifest!(root)
|
|
59
|
+
path = File.join(root, "manifest.yaml")
|
|
60
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
61
|
+
raw["lanes"].each { |lane| lane["name"] = @to if lane["name"] == @from }
|
|
62
|
+
raw["entries"].each do |entry|
|
|
63
|
+
entry["lane"] = @to if entry["lane"] == @from
|
|
64
|
+
entry["key"] = entry["key"].sub(/\A#{Regexp.escape(@from)}(\.|\z)/, "#{@to}\\1")
|
|
65
|
+
entry["path"] = entry["path"].sub(%r{\A(data/)?#{Regexp.escape(@from)}(/|\z)}, "\\1#{@to}\\2")
|
|
66
|
+
end
|
|
67
|
+
File.write(path, YAML.dump(raw))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Deps < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :deps
|
|
9
|
+
summary "List the keys a derived entry depends on (its projection/external sources)."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted key of the derived entry whose source keys you want"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(key:)
|
|
17
|
+
super()
|
|
18
|
+
@key = key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, **)
|
|
22
|
+
entry = container.manifest.data.entries.find { |e| e.key == @key }
|
|
23
|
+
deps =
|
|
24
|
+
if entry&.derived?
|
|
25
|
+
src = entry.source
|
|
26
|
+
if src.projection?
|
|
27
|
+
Array(src.select).compact
|
|
28
|
+
elsif src.external?
|
|
29
|
+
Array(src.sources).compact
|
|
30
|
+
else
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
{ "key" => @key, "deps" => deps.uniq }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.new(*args, **kwargs)
|
|
40
|
+
return super(**kwargs) unless args.any?
|
|
41
|
+
|
|
42
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
43
|
+
mapped = positional.zip(args).to_h
|
|
44
|
+
super(**mapped.merge(kwargs))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Doctor < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :doctor
|
|
9
|
+
summary "Run health checks on the textus store and report any issues."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "doctor"
|
|
12
|
+
arg :checks, Array, required: false, description: "subset of check names to run (default: all)"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(checks: nil)
|
|
17
|
+
super()
|
|
18
|
+
@checks = checks
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, call:, **)
|
|
22
|
+
Textus::Doctor.build(container: container, checks: @checks, role: call.role)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Drain < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :drain
|
|
9
|
+
summary "Seed refresh + sweep jobs then drain the queue to empty. " \
|
|
10
|
+
"Identical to one Watcher tick. Use when no watcher is running."
|
|
11
|
+
surfaces :cli, :mcp
|
|
12
|
+
arg :prefix, String, description: "restrict to keys under this dotted prefix"
|
|
13
|
+
arg :lane, String, description: "restrict to entries in this lane"
|
|
14
|
+
|
|
15
|
+
BURN = :sync
|
|
16
|
+
|
|
17
|
+
def initialize(prefix: nil, lane: nil)
|
|
18
|
+
super()
|
|
19
|
+
@prefix = prefix
|
|
20
|
+
@lane = lane
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(container:, call:)
|
|
24
|
+
queue = Textus::Ports::Queue.new(root: container.root)
|
|
25
|
+
Textus::Background::Planner::Plan.seed(
|
|
26
|
+
container: container,
|
|
27
|
+
queue: queue,
|
|
28
|
+
role: call.role,
|
|
29
|
+
)
|
|
30
|
+
queue.reclaim(now: Textus::Ports::Clock.new.now)
|
|
31
|
+
summary = Textus::Background::Worker.for(container:, queue:).drain
|
|
32
|
+
{
|
|
33
|
+
"protocol" => Textus::PROTOCOL,
|
|
34
|
+
"ok" => summary.failed.zero?,
|
|
35
|
+
"completed" => summary.completed,
|
|
36
|
+
"failed" => summary.failed,
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Enqueue < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :enqueue
|
|
9
|
+
summary "Push a registered job type onto the convergence queue, to be run by drain/serve."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "enqueue"
|
|
12
|
+
arg :type, String, required: true, positional: true,
|
|
13
|
+
description: "registered job type (e.g. materialize, re-pull, sweep)"
|
|
14
|
+
arg :args, Hash, default: {},
|
|
15
|
+
description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
|
|
16
|
+
|
|
17
|
+
BURN = :sync
|
|
18
|
+
|
|
19
|
+
def initialize(type:, args: {})
|
|
20
|
+
super()
|
|
21
|
+
@type = type
|
|
22
|
+
@job_args = args
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def args
|
|
26
|
+
{ type: @type, args: @job_args }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(container:, call:)
|
|
30
|
+
action_class = begin
|
|
31
|
+
Textus::Background::Job.fetch(@type.to_s)
|
|
32
|
+
rescue Textus::UsageError
|
|
33
|
+
raise Textus::UsageError.new("unregistered job type '#{@type}'")
|
|
34
|
+
end
|
|
35
|
+
if action_class.const_defined?(:REQUIRED_ROLE) && call.role != action_class::REQUIRED_ROLE
|
|
36
|
+
raise Textus::Error.new(
|
|
37
|
+
"forbidden",
|
|
38
|
+
"role '#{call.role}' is not authorized to enqueue this job type (requires '#{action_class::REQUIRED_ROLE}')",
|
|
39
|
+
details: { "role" => call.role, "required_role" => action_class::REQUIRED_ROLE },
|
|
40
|
+
exit_code: 77,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
job = Textus::Ports::Queue::Job.new(
|
|
45
|
+
type: @type,
|
|
46
|
+
args: @job_args,
|
|
47
|
+
enqueued_by: call.role,
|
|
48
|
+
max_attempts: 3,
|
|
49
|
+
)
|
|
50
|
+
Textus::Ports::Queue.new(root: container.root).enqueue(job)
|
|
51
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Get < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :get
|
|
9
|
+
summary "Read one entry - a pure on-disk read annotated with a freshness " \
|
|
10
|
+
"verdict; never ingests (quarantine freshness is drain + hook " \
|
|
11
|
+
"only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
|
|
12
|
+
"freshness)."
|
|
13
|
+
surfaces :cli, :mcp
|
|
14
|
+
arg :key, String, required: true, positional: true,
|
|
15
|
+
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
16
|
+
view { |v, _i| v.to_h_for_wire }
|
|
17
|
+
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(container:, call:, file_stat: Textus::Ports::Storage::FileStat.new)
|
|
26
|
+
@container = container
|
|
27
|
+
@call = call
|
|
28
|
+
@manifest = container.manifest
|
|
29
|
+
@file_store = container.file_store
|
|
30
|
+
@file_stat = file_stat
|
|
31
|
+
annotated_envelope(@key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.new(*args, **kwargs)
|
|
35
|
+
return super(**kwargs) unless args.any?
|
|
36
|
+
|
|
37
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
38
|
+
mapped = positional.zip(args).to_h
|
|
39
|
+
super(**mapped.merge(kwargs))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def annotated_envelope(key)
|
|
45
|
+
envelope = read_raw_envelope(key)
|
|
46
|
+
return nil if envelope.nil?
|
|
47
|
+
|
|
48
|
+
entry = @manifest.resolver.resolve(key).entry
|
|
49
|
+
envelope.with(freshness: evaluator.verdict(entry))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def evaluator
|
|
53
|
+
@evaluator ||= Textus::Core::Freshness::Evaluator.new(
|
|
54
|
+
manifest: @manifest,
|
|
55
|
+
file_stat: @file_stat,
|
|
56
|
+
clock: @call,
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_raw_envelope(key)
|
|
61
|
+
res = @manifest.resolver.resolve(key)
|
|
62
|
+
mentry = res.entry
|
|
63
|
+
path = res.path
|
|
64
|
+
return nil unless @file_store.exists?(path)
|
|
65
|
+
|
|
66
|
+
raw = @file_store.read(path)
|
|
67
|
+
parsed = Textus::Entry.for_format(mentry.format).parse(raw, path: path)
|
|
68
|
+
Textus::Envelope.build(
|
|
69
|
+
key: key,
|
|
70
|
+
mentry: mentry,
|
|
71
|
+
path: path,
|
|
72
|
+
meta: parsed["_meta"],
|
|
73
|
+
body: parsed["body"],
|
|
74
|
+
etag: Textus::Etag.for_bytes(raw),
|
|
75
|
+
content: parsed["content"],
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Jobs < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :jobs
|
|
9
|
+
summary "List queued jobs by state; retry a dead-lettered job or purge a state."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "jobs"
|
|
12
|
+
arg :state, String, default: "ready", description: "ready|leased|done|failed"
|
|
13
|
+
arg :action, String, default: nil, description: "retry|purge (optional)"
|
|
14
|
+
arg :job_id, String, default: nil, description: "job id (required for action=retry)"
|
|
15
|
+
|
|
16
|
+
BURN = :sync
|
|
17
|
+
|
|
18
|
+
def initialize(state: "ready", action: nil, job_id: nil)
|
|
19
|
+
super()
|
|
20
|
+
@state = state
|
|
21
|
+
@action = action
|
|
22
|
+
@job_id = job_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(container:, **)
|
|
26
|
+
queue = Textus::Ports::Queue.new(root: container.root)
|
|
27
|
+
case @action
|
|
28
|
+
when "retry"
|
|
29
|
+
queue.retry_failed(@job_id)
|
|
30
|
+
when "purge"
|
|
31
|
+
queue.purge(@state)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "state" => @state, "jobs" => queue.list(@state) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyDelete < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_delete
|
|
9
|
+
summary "Delete one entry by key. Single-key, lower blast radius than key_delete_prefix; " \
|
|
10
|
+
"guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
|
|
11
|
+
surfaces :cli, :mcp
|
|
12
|
+
cli "key delete"
|
|
13
|
+
arg :key, String, required: true, positional: true,
|
|
14
|
+
description: "dotted entry key to delete"
|
|
15
|
+
arg :if_etag, String,
|
|
16
|
+
description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
|
|
17
|
+
|
|
18
|
+
BURN = :sync
|
|
19
|
+
|
|
20
|
+
def initialize(key:, if_etag: nil)
|
|
21
|
+
super()
|
|
22
|
+
@key = key
|
|
23
|
+
@if_etag = if_etag
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(container:, call:)
|
|
27
|
+
run_with_cascade(@key, container:, call:) do
|
|
28
|
+
Textus::Manifest::Data.validate_key!(@key)
|
|
29
|
+
mentry = container.manifest.resolver.resolve(@key).entry
|
|
30
|
+
|
|
31
|
+
auth(container).check_action!(action: :key_delete, actor: call.role, key: @key, extra: { if_etag: @if_etag })
|
|
32
|
+
|
|
33
|
+
writer(container, call).delete(@key, mentry:, if_etag: @if_etag)
|
|
34
|
+
|
|
35
|
+
container.steps.publish(
|
|
36
|
+
:entry_deleted,
|
|
37
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
38
|
+
key: @key,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => @key, "deleted" => true }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyDeletePrefix < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_delete_prefix
|
|
9
|
+
summary "Bulk-delete every leaf key under prefix."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "key delete-prefix"
|
|
12
|
+
arg :prefix, String, required: true, positional: true,
|
|
13
|
+
description: "every leaf key under this dotted prefix is deleted"
|
|
14
|
+
arg :dry_run, :boolean, default: false,
|
|
15
|
+
description: "when true, returns the keys that would be deleted without deleting them; " \
|
|
16
|
+
"defaults to false, so omitting it deletes immediately"
|
|
17
|
+
view { |v, _i| v.to_h }
|
|
18
|
+
|
|
19
|
+
BURN = :sync
|
|
20
|
+
|
|
21
|
+
def initialize(prefix:, dry_run: false)
|
|
22
|
+
super()
|
|
23
|
+
@prefix = prefix
|
|
24
|
+
@dry_run = dry_run
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(container:, call:)
|
|
28
|
+
raise UsageError.new("prefix required") if @prefix.nil? || @prefix.empty?
|
|
29
|
+
|
|
30
|
+
leaves = Textus::Action::List.new(prefix: @prefix).call(container: container)
|
|
31
|
+
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
32
|
+
|
|
33
|
+
warnings = leaves.empty? ? ["no keys under #{@prefix}"] : []
|
|
34
|
+
steps = leaves.map { |key| { "op" => "delete", "key" => key } }
|
|
35
|
+
|
|
36
|
+
plan = Textus::Background::Plan.new(steps: steps, warnings: warnings)
|
|
37
|
+
return plan if @dry_run
|
|
38
|
+
|
|
39
|
+
steps.each do |step|
|
|
40
|
+
Textus::Action::KeyDelete.new(key: step["key"]).call(container: container, call: call)
|
|
41
|
+
end
|
|
42
|
+
plan
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyMv < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_mv
|
|
9
|
+
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "key mv"
|
|
12
|
+
arg :old_key, String, required: true, positional: true,
|
|
13
|
+
description: "current dotted key"
|
|
14
|
+
arg :new_key, String, required: true, positional: true,
|
|
15
|
+
description: "new dotted key (must be the same zone and format as old_key)"
|
|
16
|
+
arg :dry_run, :boolean,
|
|
17
|
+
description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
|
|
18
|
+
"defaults to false, so omitting it applies the move immediately " \
|
|
19
|
+
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
20
|
+
|
|
21
|
+
BURN = :sync
|
|
22
|
+
|
|
23
|
+
def initialize(old_key:, new_key:, dry_run: false)
|
|
24
|
+
super()
|
|
25
|
+
@old_key = old_key
|
|
26
|
+
@new_key = new_key
|
|
27
|
+
@dry_run = dry_run
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(container:, call:)
|
|
31
|
+
run_with_cascade(cascade_target_key, container:, call:) do
|
|
32
|
+
execute_move(container, call)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def cascade_target_key
|
|
39
|
+
@dry_run ? nil : @new_key
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def execute_move(container, call)
|
|
43
|
+
old_res, new_res = prepare(container, call)
|
|
44
|
+
return dry_run_result(container, old_res, new_res) if @dry_run
|
|
45
|
+
|
|
46
|
+
envelope = apply_move(container, call, old_res, new_res)
|
|
47
|
+
publish_rename(container, call, envelope)
|
|
48
|
+
success_result(old_res, new_res, envelope)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def apply_move(container, call, old_res, new_res)
|
|
52
|
+
ensure_uid!(container, call, old_res.entry)
|
|
53
|
+
writer(container, call).move(
|
|
54
|
+
from_key: @old_key,
|
|
55
|
+
to_key: @new_key,
|
|
56
|
+
new_mentry: new_res.entry,
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def publish_rename(container, call, envelope)
|
|
61
|
+
container.steps.publish(
|
|
62
|
+
:entry_renamed,
|
|
63
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
64
|
+
key: @new_key,
|
|
65
|
+
from_key: @old_key,
|
|
66
|
+
to_key: @new_key,
|
|
67
|
+
envelope: envelope,
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def success_result(old_res, new_res, envelope)
|
|
72
|
+
{
|
|
73
|
+
"protocol" => PROTOCOL,
|
|
74
|
+
"ok" => true,
|
|
75
|
+
"from_key" => @old_key,
|
|
76
|
+
"to_key" => @new_key,
|
|
77
|
+
"from_path" => old_res.path,
|
|
78
|
+
"to_path" => new_res.path,
|
|
79
|
+
"uid" => envelope.uid,
|
|
80
|
+
"envelope" => envelope.to_h_for_wire,
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def prepare(container, call)
|
|
85
|
+
Textus::Manifest::Data.validate_key!(@old_key)
|
|
86
|
+
Textus::Manifest::Data.validate_key!(@new_key)
|
|
87
|
+
raise UsageError.new("mv: old and new keys are identical") if @old_key == @new_key
|
|
88
|
+
|
|
89
|
+
old_res = container.manifest.resolver.resolve(@old_key)
|
|
90
|
+
new_res = container.manifest.resolver.resolve(@new_key)
|
|
91
|
+
raise UnknownKey.new(@old_key) unless reader(container).exists?(@old_key)
|
|
92
|
+
|
|
93
|
+
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
94
|
+
auth(container).check_action!(action: :key_mv, actor: call.role, key: @old_key)
|
|
95
|
+
auth(container).check_action!(action: :key_mv, actor: call.role, key: @new_key)
|
|
96
|
+
raise UsageError.new("mv: target '#{@new_key}' already exists at #{new_res.path}") if reader(container).exists?(@new_key)
|
|
97
|
+
|
|
98
|
+
[old_res, new_res]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_zone_and_format!(old_mentry, new_mentry)
|
|
102
|
+
if old_mentry.lane != new_mentry.lane
|
|
103
|
+
raise UsageError.new(
|
|
104
|
+
"mv: cross-zone move refused (#{old_mentry.lane} -> #{new_mentry.lane}). " \
|
|
105
|
+
"Use put+delete for cross-zone moves.",
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
return if old_mentry.format == new_mentry.format
|
|
109
|
+
|
|
110
|
+
raise UsageError.new("mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ensure_uid!(container, call, old_mentry)
|
|
114
|
+
pre_env = reader(container).read(@old_key)
|
|
115
|
+
return if pre_env.uid
|
|
116
|
+
|
|
117
|
+
writer(container, call).put(
|
|
118
|
+
@old_key,
|
|
119
|
+
mentry: old_mentry,
|
|
120
|
+
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
121
|
+
meta: pre_env.meta,
|
|
122
|
+
body: pre_env.body,
|
|
123
|
+
content: pre_env.content,
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def dry_run_result(container, old_res, new_res)
|
|
129
|
+
pre_env = reader(container).read(@old_key)
|
|
130
|
+
{
|
|
131
|
+
"protocol" => PROTOCOL,
|
|
132
|
+
"ok" => true,
|
|
133
|
+
"dry_run" => true,
|
|
134
|
+
"from_key" => @old_key,
|
|
135
|
+
"to_key" => @new_key,
|
|
136
|
+
"from_path" => old_res.path,
|
|
137
|
+
"to_path" => new_res.path,
|
|
138
|
+
"uid" => pre_env.uid,
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class KeyMvPrefix < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :key_mv_prefix
|
|
9
|
+
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "key mv-prefix"
|
|
12
|
+
arg :from_prefix, String, required: true, positional: true,
|
|
13
|
+
description: "dotted prefix whose leaf keys are renamed"
|
|
14
|
+
arg :to_prefix, String, required: true, positional: true,
|
|
15
|
+
description: "dotted prefix the keys are renamed to"
|
|
16
|
+
arg :dry_run, :boolean, default: false,
|
|
17
|
+
description: "when true, returns the planned moves without applying them; defaults " \
|
|
18
|
+
"to false, so omitting it applies the rename immediately"
|
|
19
|
+
view { |v, _i| v.to_h }
|
|
20
|
+
|
|
21
|
+
BURN = :sync
|
|
22
|
+
|
|
23
|
+
def initialize(from_prefix:, to_prefix:, dry_run: false)
|
|
24
|
+
super()
|
|
25
|
+
@from_prefix = from_prefix
|
|
26
|
+
@to_prefix = to_prefix
|
|
27
|
+
@dry_run = dry_run
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(container:, call:)
|
|
31
|
+
raise UsageError.new("from_prefix and to_prefix required") if @from_prefix.nil? || @to_prefix.nil?
|
|
32
|
+
|
|
33
|
+
leaves = Textus::Action::List.new(prefix: @from_prefix).call(container: container)
|
|
34
|
+
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
35
|
+
|
|
36
|
+
if leaves.include?(@from_prefix)
|
|
37
|
+
raise UsageError.new("from_prefix '#{@from_prefix}' is itself a leaf — use `mv` to rename a single key")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
warnings = []
|
|
41
|
+
warnings << "no keys under #{@from_prefix}" if leaves.empty?
|
|
42
|
+
|
|
43
|
+
steps = leaves.map do |old_key|
|
|
44
|
+
tail = old_key.delete_prefix("#{@from_prefix}.")
|
|
45
|
+
new_key = "#{@to_prefix}.#{tail}"
|
|
46
|
+
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
plan = Textus::Background::Plan.new(steps: steps, warnings: warnings)
|
|
50
|
+
return plan if @dry_run
|
|
51
|
+
|
|
52
|
+
steps.each do |step|
|
|
53
|
+
Textus::Action::KeyMv.new(old_key: step["from"], new_key: step["to"]).call(container: container, call: call)
|
|
54
|
+
end
|
|
55
|
+
plan
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|