textus 0.47.1 → 0.49.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 +32 -0
- data/README.md +9 -7
- data/SPEC.md +40 -46
- data/docs/reference/conventions.md +11 -10
- data/lib/textus/boot.rb +2 -2
- data/lib/textus/cli/runner.rb +5 -4
- 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/rules.rb +11 -23
- data/lib/textus/manifest/schema.rb +3 -18
- 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/freshness.rb +29 -22
- 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,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
|