textus 0.51.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 +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- 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/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- 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 +13 -13
- 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 +2 -2
- 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/sentinels.rb +2 -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 +19 -11
- 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 +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- 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 +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- 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 +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- 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 +17 -63
- 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/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- 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/role.rb +1 -1
- 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 -4
- 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 +133 -107
- 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.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- 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/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- 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 -69
- 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/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/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,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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class List < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :list
|
|
9
|
+
summary "List keys filtered by lane and/or prefix."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :prefix, String,
|
|
12
|
+
description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
|
|
13
|
+
arg :lane, String,
|
|
14
|
+
description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
|
|
15
|
+
view(:cli) { |rows| { "entries" => rows } }
|
|
16
|
+
|
|
17
|
+
BURN = :sync
|
|
18
|
+
|
|
19
|
+
def initialize(prefix: nil, lane: nil)
|
|
20
|
+
super()
|
|
21
|
+
@prefix = prefix
|
|
22
|
+
@lane = lane
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
26
|
+
manifest = container.manifest
|
|
27
|
+
rows = manifest.resolver.enumerate(prefix: @prefix)
|
|
28
|
+
rows = rows.select { |row| row[:manifest_entry].lane == @lane } if @lane
|
|
29
|
+
rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.new(*args, **kwargs)
|
|
33
|
+
return super(**kwargs) unless args.any?
|
|
34
|
+
|
|
35
|
+
call_spec = instance_method(:initialize).parameters
|
|
36
|
+
required = call_spec.slice(:keyreq).map(&:last)
|
|
37
|
+
optional = call_spec.slice(:key).map(&:last)
|
|
38
|
+
positional = required + optional
|
|
39
|
+
mapped = positional.zip(args).to_h
|
|
40
|
+
super(**mapped.merge(kwargs))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Propose < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :propose
|
|
9
|
+
summary "Write a proposal to the role's propose_lane. Auto-prefixes the key."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli_stdin :json
|
|
12
|
+
arg :key, String, required: true, positional: true,
|
|
13
|
+
description: "key relative to propose_lane, e.g. 'decisions.feature-x'"
|
|
14
|
+
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
15
|
+
description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
|
|
16
|
+
arg :body, String,
|
|
17
|
+
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
18
|
+
arg :content, Hash,
|
|
19
|
+
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
20
|
+
view { |env, _i| env.to_h_for_wire }
|
|
21
|
+
|
|
22
|
+
BURN = :sync
|
|
23
|
+
|
|
24
|
+
def initialize(key:, meta: nil, body: nil, content: nil)
|
|
25
|
+
super()
|
|
26
|
+
@key = key
|
|
27
|
+
@meta = meta
|
|
28
|
+
@body = body
|
|
29
|
+
@content = content
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(container:, call:)
|
|
33
|
+
zone = container.manifest.policy.propose_lane_for(call.role)
|
|
34
|
+
unless zone
|
|
35
|
+
raise Textus::Error.new(
|
|
36
|
+
"propose_forbidden",
|
|
37
|
+
"role '#{call.role}' has no writable propose_lane",
|
|
38
|
+
details: { "role" => call.role },
|
|
39
|
+
hint: "the manifest must define a queue zone and '#{call.role}' must hold the 'propose' capability",
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
run_with_cascade("#{zone}.#{@key}", container:, call:) do
|
|
44
|
+
Textus::Action::Put.new(
|
|
45
|
+
key: "#{zone}.#{@key}",
|
|
46
|
+
meta: @meta || {},
|
|
47
|
+
body: @body,
|
|
48
|
+
content: @content,
|
|
49
|
+
).call(container: container, call: call)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Published < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :published
|
|
9
|
+
summary "List all entries that declare a publish_to target."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "published"
|
|
12
|
+
|
|
13
|
+
BURN = :sync
|
|
14
|
+
|
|
15
|
+
def args
|
|
16
|
+
{}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(container:, **)
|
|
20
|
+
container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
21
|
+
{ "key" => entry.key, "publish_to" => entry.publish_to }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Action
|
|
7
|
+
class Pulse
|
|
8
|
+
class Scanner
|
|
9
|
+
def initialize(prefix: nil, lane: nil)
|
|
10
|
+
@prefix = prefix
|
|
11
|
+
@lane = lane
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(container:, call:)
|
|
15
|
+
@container = container
|
|
16
|
+
@call = call
|
|
17
|
+
@manifest = container.manifest
|
|
18
|
+
@file_store = container.file_store
|
|
19
|
+
|
|
20
|
+
rows = []
|
|
21
|
+
@manifest.data.entries.each do |mentry|
|
|
22
|
+
next if @prefix && !mentry.key.start_with?(@prefix)
|
|
23
|
+
next if @lane && mentry.lane != @lane
|
|
24
|
+
|
|
25
|
+
rows << row_for(mentry)
|
|
26
|
+
end
|
|
27
|
+
rows
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def row_for(mentry)
|
|
33
|
+
envelope = safe_get(mentry.key)
|
|
34
|
+
last = envelope&.meta&.dig("last_fetched_at")
|
|
35
|
+
ttl, action = policy_for(mentry)
|
|
36
|
+
return base_row(mentry, last).merge(status: :no_policy) if ttl.nil?
|
|
37
|
+
|
|
38
|
+
basis = basis_for(mentry)
|
|
39
|
+
expired = expired?(mentry, basis, ttl)
|
|
40
|
+
base_row(mentry, last).merge(
|
|
41
|
+
ttl_seconds: ttl,
|
|
42
|
+
action: action,
|
|
43
|
+
status: expired ? :expired : :fresh,
|
|
44
|
+
next_due_at: basis.nil? ? nil : (basis + ttl).utc.iso8601,
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def policy_for(mentry)
|
|
49
|
+
if mentry.intake?
|
|
50
|
+
ttl = mentry.source.ttl_seconds
|
|
51
|
+
return [ttl, :refresh] unless ttl.nil?
|
|
52
|
+
end
|
|
53
|
+
ret = @manifest.rules.for(mentry.key).retention
|
|
54
|
+
return [ret.ttl_seconds, ret.action] unless ret.nil?
|
|
55
|
+
|
|
56
|
+
[nil, nil]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def basis_for(mentry)
|
|
60
|
+
return evaluator.intake_basis(mentry) if mentry.intake? && mentry.source.ttl_seconds
|
|
61
|
+
|
|
62
|
+
mtime_for(mentry.key)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def expired?(mentry, basis, ttl)
|
|
66
|
+
if mentry.intake? && mentry.source.ttl_seconds
|
|
67
|
+
evaluator.verdict(mentry).stale
|
|
68
|
+
else
|
|
69
|
+
basis.nil? || Textus::Core::Retention::Sweep.expired?(ttl_seconds: ttl, mtime: basis, now: @call.now)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def evaluator
|
|
74
|
+
@evaluator ||= Textus::Core::Freshness::Evaluator.new(
|
|
75
|
+
manifest: @manifest,
|
|
76
|
+
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
77
|
+
clock: @call,
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def mtime_for(key)
|
|
82
|
+
path = @manifest.resolver.resolve(key).path
|
|
83
|
+
@file_store.exists?(path) ? Textus::Ports::Storage::FileStat.new.mtime(path) : nil
|
|
84
|
+
rescue Textus::Error
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def base_row(mentry, last)
|
|
89
|
+
{
|
|
90
|
+
key: mentry.key,
|
|
91
|
+
lane: mentry.lane,
|
|
92
|
+
last_fetched_at: last,
|
|
93
|
+
age_seconds: last ? (@call.now - Time.parse(last)).to_i : nil,
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def safe_get(key)
|
|
98
|
+
res = @manifest.resolver.resolve(key)
|
|
99
|
+
return nil unless @file_store.exists?(res.path)
|
|
100
|
+
|
|
101
|
+
raw = @file_store.read(res.path)
|
|
102
|
+
parsed = Textus::Entry.for_format(res.entry.format).parse(raw, path: res.path)
|
|
103
|
+
Textus::Envelope.build(
|
|
104
|
+
key: key,
|
|
105
|
+
mentry: res.entry,
|
|
106
|
+
path: res.path,
|
|
107
|
+
meta: parsed["_meta"],
|
|
108
|
+
body: parsed["body"],
|
|
109
|
+
etag: Textus::Etag.for_bytes(raw),
|
|
110
|
+
content: parsed["content"],
|
|
111
|
+
)
|
|
112
|
+
rescue Textus::Error
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Action
|
|
7
|
+
class Pulse < Base
|
|
8
|
+
extend Textus::Contract::DSL
|
|
9
|
+
|
|
10
|
+
verb :pulse
|
|
11
|
+
summary "Delta since cursor — changed entries, stale, pending proposals, doctor summary."
|
|
12
|
+
surfaces :cli, :mcp
|
|
13
|
+
around :cursor
|
|
14
|
+
arg :since, Integer, session_default: :cursor,
|
|
15
|
+
description: "audit seq to diff from; defaults to the session cursor"
|
|
16
|
+
|
|
17
|
+
BURN = :sync
|
|
18
|
+
|
|
19
|
+
def initialize(since: nil)
|
|
20
|
+
super()
|
|
21
|
+
@since = since
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(container:, call:)
|
|
25
|
+
@container = container
|
|
26
|
+
@call = call
|
|
27
|
+
@manifest = container.manifest
|
|
28
|
+
@audit_log = container.audit_log
|
|
29
|
+
@root = container.root
|
|
30
|
+
@steps = container.steps
|
|
31
|
+
|
|
32
|
+
freshness_rows = Pulse::Scanner.new.call(container: container, call: call)
|
|
33
|
+
{
|
|
34
|
+
"cursor" => @audit_log.latest_seq,
|
|
35
|
+
"changed" => Textus::Action::Audit.new(seq_since: @since).call(container: container),
|
|
36
|
+
"stale" => freshness_rows.select { |row| row[:status] == :expired }.map { |row| row[:key] },
|
|
37
|
+
"pending_review" => review_keys,
|
|
38
|
+
"doctor" => doctor_summary,
|
|
39
|
+
"contract_etag" => Textus::Etag.for_contract(@root),
|
|
40
|
+
"next_due_at" => soonest_due(freshness_rows),
|
|
41
|
+
"hook_errors" => hook_errors_since(@since || 0),
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def soonest_due(rows)
|
|
48
|
+
times = rows.map { |row| row[:next_due_at] }.compact.map { |t| Time.parse(t) }
|
|
49
|
+
return nil if times.empty?
|
|
50
|
+
|
|
51
|
+
times.min.utc.iso8601
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def review_keys
|
|
55
|
+
queue = @manifest.policy.queue_lane
|
|
56
|
+
return [] unless queue
|
|
57
|
+
|
|
58
|
+
rows = Textus::Action::List.new(lane: queue).call(container: @container)
|
|
59
|
+
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def doctor_summary
|
|
63
|
+
result = Textus::Doctor.build(container: @container)
|
|
64
|
+
issues = result["issues"] || []
|
|
65
|
+
{
|
|
66
|
+
"ok" => result["ok"],
|
|
67
|
+
"warn" => issues.count { |i| i["level"] == "warning" },
|
|
68
|
+
"fail" => issues.count { |i| i["level"] == "error" },
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def hook_errors_since(seq)
|
|
73
|
+
@steps.error_log.since(seq).map do |row|
|
|
74
|
+
{
|
|
75
|
+
"seq" => row[:seq],
|
|
76
|
+
"event" => row[:event].to_s,
|
|
77
|
+
"hook" => row[:hook].to_s,
|
|
78
|
+
"key" => row[:key],
|
|
79
|
+
"error_class" => row[:error_class],
|
|
80
|
+
"error_message" => row[:error_message],
|
|
81
|
+
"at" => row[:at],
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Put < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :put
|
|
9
|
+
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
|
|
13
|
+
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
14
|
+
description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
|
|
15
|
+
arg :body, String,
|
|
16
|
+
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
17
|
+
arg :content, Hash,
|
|
18
|
+
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
19
|
+
arg :if_etag, String,
|
|
20
|
+
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
21
|
+
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
22
|
+
|
|
23
|
+
BURN = :sync
|
|
24
|
+
|
|
25
|
+
def initialize(key:, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
26
|
+
super()
|
|
27
|
+
@key = key
|
|
28
|
+
@meta = meta
|
|
29
|
+
@body = body
|
|
30
|
+
@content = content
|
|
31
|
+
@if_etag = if_etag
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(container:, call:)
|
|
35
|
+
run_with_cascade(@key, container:, call:) do
|
|
36
|
+
Textus::Manifest::Data.validate_key!(@key)
|
|
37
|
+
mentry = container.manifest.resolver.resolve(@key).entry
|
|
38
|
+
auth(container).check_action!(action: :put, actor: call.role, key: @key, extra: { if_etag: @if_etag })
|
|
39
|
+
|
|
40
|
+
envelope = writer(container, call).put(
|
|
41
|
+
@key,
|
|
42
|
+
mentry: mentry,
|
|
43
|
+
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
44
|
+
meta: @meta,
|
|
45
|
+
body: @body,
|
|
46
|
+
content: @content,
|
|
47
|
+
),
|
|
48
|
+
if_etag: @if_etag,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
container.steps.publish(
|
|
52
|
+
:entry_written,
|
|
53
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
54
|
+
key: @key,
|
|
55
|
+
envelope: envelope,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
envelope
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Rdeps < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :rdeps
|
|
9
|
+
summary "List the derived entries that depend on a key (reverse deps / impact set)."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(key:)
|
|
17
|
+
super()
|
|
18
|
+
@key = key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, **)
|
|
22
|
+
manifest = container.manifest
|
|
23
|
+
rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
|
|
24
|
+
next unless entry.derived?
|
|
25
|
+
|
|
26
|
+
src = entry.source
|
|
27
|
+
sources =
|
|
28
|
+
if src.projection?
|
|
29
|
+
Array(src.select).compact
|
|
30
|
+
elsif src.external?
|
|
31
|
+
Array(src.sources).compact
|
|
32
|
+
else
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
acc << entry.key if sources.any? { |source| source == @key || @key.start_with?("#{source}.") }
|
|
36
|
+
end
|
|
37
|
+
{ "key" => @key, "rdeps" => rdeps }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.new(*args, **kwargs)
|
|
41
|
+
return super(**kwargs) unless args.any?
|
|
42
|
+
|
|
43
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
44
|
+
mapped = positional.zip(args).to_h
|
|
45
|
+
super(**mapped.merge(kwargs))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Reject < WriteVerb
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :reject
|
|
9
|
+
summary "discard a queued proposal without applying it"
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "reject"
|
|
12
|
+
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(pending_key:)
|
|
17
|
+
super()
|
|
18
|
+
@pending_key = pending_key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, call:)
|
|
22
|
+
run_with_cascade(@pending_key, container:, call:) do
|
|
23
|
+
auth = Textus::Gate::Auth.new(container)
|
|
24
|
+
auth.check_action!(action: :reject, actor: call.role, key: @pending_key)
|
|
25
|
+
|
|
26
|
+
mentry = container.manifest.resolver.resolve(@pending_key).entry
|
|
27
|
+
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
28
|
+
raise ProposalError.new("reject: '#{@pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
env = Textus::Action::Get.new(key: @pending_key).call(container: container, call: call)
|
|
32
|
+
proposal = env.meta&.dig("proposal") or raise ProposalError.new("entry has no proposal block: #{@pending_key}")
|
|
33
|
+
target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
34
|
+
|
|
35
|
+
writer(container, call).delete(@pending_key, mentry: mentry)
|
|
36
|
+
|
|
37
|
+
container.steps.publish(
|
|
38
|
+
:proposal_rejected,
|
|
39
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
40
|
+
key: @pending_key,
|
|
41
|
+
target_key: target_key,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
{ "protocol" => PROTOCOL, "rejected" => @pending_key, "target_key" => target_key }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|