textus 0.54.2 → 0.55.1
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 +8 -1
- data/SPEC.md +27 -0
- data/docs/architecture/README.md +20 -8
- data/docs/reference/conventions.md +1 -1
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +23 -21
- data/lib/textus/action/audit.rb +24 -61
- data/lib/textus/action/base.rb +9 -9
- data/lib/textus/action/blame.rb +18 -36
- data/lib/textus/action/boot.rb +2 -4
- data/lib/textus/action/data_mv.rb +20 -31
- data/lib/textus/action/deps.rb +3 -18
- data/lib/textus/action/doctor.rb +2 -9
- data/lib/textus/action/drain.rb +11 -19
- data/lib/textus/action/enqueue.rb +14 -30
- data/lib/textus/action/get.rb +12 -56
- data/lib/textus/action/ingest.rb +74 -78
- data/lib/textus/action/jobs.rb +6 -15
- data/lib/textus/action/key_delete.rb +6 -16
- data/lib/textus/action/key_delete_prefix.rb +8 -17
- data/lib/textus/action/key_mv.rb +54 -61
- data/lib/textus/action/key_mv_prefix.rb +13 -22
- data/lib/textus/action/list.rb +7 -21
- data/lib/textus/action/propose.rb +16 -26
- data/lib/textus/action/published.rb +3 -5
- data/lib/textus/action/pulse.rb +19 -26
- data/lib/textus/action/put.rb +15 -29
- data/lib/textus/action/rdeps.rb +3 -18
- data/lib/textus/action/reject.rb +12 -21
- data/lib/textus/action/rule_explain.rb +12 -22
- data/lib/textus/action/rule_lint.rb +10 -16
- data/lib/textus/action/rule_list.rb +5 -9
- data/lib/textus/action/schema_envelope.rb +3 -10
- data/lib/textus/action/uid.rb +3 -17
- data/lib/textus/action/where.rb +3 -18
- data/lib/textus/boot.rb +7 -15
- data/lib/textus/contract/arg.rb +10 -0
- data/lib/textus/contract/dsl.rb +88 -0
- data/lib/textus/contract/spec.rb +25 -0
- data/lib/textus/contract.rb +0 -162
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +4 -7
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +6 -0
- data/lib/textus/format/base.rb +0 -4
- data/lib/textus/format/json.rb +5 -6
- data/lib/textus/format/markdown.rb +5 -6
- data/lib/textus/format/shared.rb +17 -0
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -6
- data/lib/textus/format.rb +6 -0
- data/lib/textus/gate/auth.rb +2 -17
- data/lib/textus/gate/binder.rb +50 -0
- data/lib/textus/gate.rb +64 -88
- data/lib/textus/init.rb +2 -4
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +3 -3
- data/lib/textus/manifest/entry/base.rb +1 -1
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/meta.rb +54 -0
- data/lib/textus/{ports → port}/audit_log.rb +44 -4
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{ports → port}/sentinel_store.rb +3 -3
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/{ports → port}/storage/file_store.rb +2 -2
- data/lib/textus/port/store.rb +93 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -1
- data/lib/textus/schema/tools.rb +11 -7
- data/lib/textus/store/compositor.rb +34 -0
- data/lib/textus/store/container.rb +43 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/envelope/reader.rb +43 -0
- data/lib/textus/store/envelope/writer.rb +195 -0
- data/lib/textus/store/geometry.rb +81 -0
- data/lib/textus/store/index/builder.rb +74 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +104 -0
- data/lib/textus/store/jobs/queue.rb +154 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention.rb +50 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/session.rb +37 -0
- data/lib/textus/store.rb +21 -13
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +10 -63
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +4 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +4 -4
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/{surfaces → surface}/cli/verb/put.rb +6 -11
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/catalog.rb +9 -26
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -1
- data/lib/textus/{surfaces → surface}/mcp/server.rb +5 -5
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/surface/projector.rb +27 -0
- data/lib/textus/{surfaces → surface}/role_scope.rb +1 -1
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/value/envelope.rb +89 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/result.rb +26 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +4 -18
- data/lib/textus.rb +9 -10
- metadata +100 -63
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/role.rb +0 -36
- data/lib/textus/session.rb +0 -35
- data/lib/textus/types.rb +0 -15
data/lib/textus/action/key_mv.rb
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
|
-
class KeyMv <
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
5
|
+
class KeyMv < Base
|
|
8
6
|
verb :key_mv
|
|
9
7
|
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
10
8
|
surfaces :cli, :mcp
|
|
@@ -18,48 +16,40 @@ module Textus
|
|
|
18
16
|
"defaults to false, so omitting it applies the move immediately " \
|
|
19
17
|
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
20
18
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
@old_key = old_key
|
|
24
|
-
@new_key = new_key
|
|
25
|
-
@dry_run = dry_run
|
|
19
|
+
def self.call(container:, call:, old_key:, new_key:, dry_run: false)
|
|
20
|
+
execute_move(container: container, call: call, old_key: old_key, new_key: new_key, dry_run: dry_run)
|
|
26
21
|
end
|
|
27
22
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
32
|
-
end
|
|
23
|
+
def self.execute_move(container:, call:, old_key:, new_key:, dry_run:)
|
|
24
|
+
prepared = prepare(container: container, old_key: old_key, new_key: new_key)
|
|
25
|
+
return prepared if prepared.is_a?(Dry::Monads::Result::Failure)
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def execute_move(container, call)
|
|
41
|
-
old_res, new_res = prepare(container, call)
|
|
42
|
-
return dry_run_result(container, old_res, new_res) if @dry_run
|
|
27
|
+
old_res, new_res = prepared
|
|
28
|
+
if dry_run
|
|
29
|
+
return Success(dry_run_result(container: container, old_key: old_key, new_key: new_key, old_res: old_res,
|
|
30
|
+
new_res: new_res))
|
|
31
|
+
end
|
|
43
32
|
|
|
44
|
-
envelope = apply_move(container, call, old_res, new_res)
|
|
45
|
-
success_result(old_res, new_res, envelope)
|
|
33
|
+
envelope = apply_move(container: container, call: call, old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res)
|
|
34
|
+
Success(success_result(old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res, envelope: envelope))
|
|
46
35
|
end
|
|
47
36
|
|
|
48
|
-
def apply_move(container
|
|
49
|
-
ensure_uid!(container, call, old_res.entry)
|
|
50
|
-
|
|
51
|
-
from_key:
|
|
52
|
-
to_key:
|
|
37
|
+
def self.apply_move(container:, call:, old_key:, new_key:, old_res:, new_res:)
|
|
38
|
+
ensure_uid!(container: container, call: call, old_key: old_key, old_mentry: old_res.entry)
|
|
39
|
+
container.compositor.move(
|
|
40
|
+
from_key: old_key,
|
|
41
|
+
to_key: new_key,
|
|
53
42
|
new_mentry: new_res.entry,
|
|
43
|
+
call: call,
|
|
54
44
|
)
|
|
55
45
|
end
|
|
56
46
|
|
|
57
|
-
def success_result(old_res
|
|
47
|
+
def self.success_result(old_key:, new_key:, old_res:, new_res:, envelope:)
|
|
58
48
|
{
|
|
59
|
-
"protocol" => PROTOCOL,
|
|
49
|
+
"protocol" => Textus::PROTOCOL,
|
|
60
50
|
"ok" => true,
|
|
61
|
-
"from_key" =>
|
|
62
|
-
"to_key" =>
|
|
51
|
+
"from_key" => old_key,
|
|
52
|
+
"to_key" => new_key,
|
|
63
53
|
"from_path" => old_res.path,
|
|
64
54
|
"to_path" => new_res.path,
|
|
65
55
|
"uid" => envelope.uid,
|
|
@@ -67,58 +57,61 @@ module Textus
|
|
|
67
57
|
}
|
|
68
58
|
end
|
|
69
59
|
|
|
70
|
-
def prepare(container
|
|
71
|
-
Textus::Manifest::Data.validate_key!(
|
|
72
|
-
Textus::Manifest::Data.validate_key!(
|
|
73
|
-
|
|
60
|
+
def self.prepare(container:, old_key:, new_key:)
|
|
61
|
+
Textus::Manifest::Data.validate_key!(old_key)
|
|
62
|
+
Textus::Manifest::Data.validate_key!(new_key)
|
|
63
|
+
return Failure(code: :usage_error, message: "mv: old and new keys are identical") if old_key == new_key
|
|
74
64
|
|
|
75
|
-
old_res = container.manifest.resolver.resolve(
|
|
76
|
-
new_res = container.manifest.resolver.resolve(
|
|
77
|
-
|
|
65
|
+
old_res = container.manifest.resolver.resolve(old_key)
|
|
66
|
+
new_res = container.manifest.resolver.resolve(new_key)
|
|
67
|
+
return Failure(code: :not_found, message: "source key '#{old_key}' not found") unless container.compositor.exists?(old_key)
|
|
78
68
|
|
|
79
|
-
validate_zone_and_format
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
69
|
+
zone_check = validate_zone_and_format(old_mentry: old_res.entry, new_mentry: new_res.entry)
|
|
70
|
+
return zone_check if zone_check.is_a?(Dry::Monads::Result::Failure)
|
|
71
|
+
|
|
72
|
+
if container.compositor.exists?(new_key)
|
|
73
|
+
return Failure(code: :usage_error, message: "mv: target '#{new_key}' already exists at #{new_res.path}")
|
|
74
|
+
end
|
|
83
75
|
|
|
84
76
|
[old_res, new_res]
|
|
85
77
|
end
|
|
86
78
|
|
|
87
|
-
def validate_zone_and_format
|
|
79
|
+
def self.validate_zone_and_format(old_mentry:, new_mentry:)
|
|
88
80
|
if old_mentry.lane != new_mentry.lane
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
81
|
+
return Failure(code: :usage_error,
|
|
82
|
+
message: "mv: cross-zone move refused (#{old_mentry.lane} -> #{new_mentry.lane}). " \
|
|
83
|
+
"Use put+delete for cross-zone moves.")
|
|
93
84
|
end
|
|
94
|
-
return
|
|
85
|
+
return unless old_mentry.format != new_mentry.format
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
Failure(code: :usage_error,
|
|
88
|
+
message: "mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
|
|
97
89
|
end
|
|
98
90
|
|
|
99
|
-
def ensure_uid!(container
|
|
100
|
-
pre_env =
|
|
91
|
+
def self.ensure_uid!(container:, call:, old_key:, old_mentry:)
|
|
92
|
+
pre_env = container.compositor.read(old_key)
|
|
101
93
|
return if pre_env.uid
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
container.compositor.write(
|
|
96
|
+
old_key,
|
|
105
97
|
mentry: old_mentry,
|
|
106
|
-
payload: Textus::Envelope::Writer::Payload.new(
|
|
98
|
+
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
107
99
|
meta: pre_env.meta,
|
|
108
100
|
body: pre_env.body,
|
|
109
101
|
content: pre_env.content,
|
|
110
102
|
),
|
|
103
|
+
call: call,
|
|
111
104
|
)
|
|
112
105
|
end
|
|
113
106
|
|
|
114
|
-
def dry_run_result(container
|
|
115
|
-
pre_env =
|
|
107
|
+
def self.dry_run_result(container:, old_key:, new_key:, old_res:, new_res:)
|
|
108
|
+
pre_env = container.compositor.read(old_key)
|
|
116
109
|
{
|
|
117
|
-
"protocol" => PROTOCOL,
|
|
110
|
+
"protocol" => Textus::PROTOCOL,
|
|
118
111
|
"ok" => true,
|
|
119
112
|
"dry_run" => true,
|
|
120
|
-
"from_key" =>
|
|
121
|
-
"to_key" =>
|
|
113
|
+
"from_key" => old_key,
|
|
114
|
+
"to_key" => new_key,
|
|
122
115
|
"from_path" => old_res.path,
|
|
123
116
|
"to_path" => new_res.path,
|
|
124
117
|
"uid" => pre_env.uid,
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
5
|
class KeyMvPrefix < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
6
|
verb :key_mv_prefix
|
|
9
7
|
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
10
8
|
surfaces :cli, :mcp
|
|
@@ -18,39 +16,32 @@ module Textus
|
|
|
18
16
|
"to false, so omitting it applies the rename immediately"
|
|
19
17
|
view { |v, _i| v.to_h }
|
|
20
18
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
@from_prefix = from_prefix
|
|
24
|
-
@to_prefix = to_prefix
|
|
25
|
-
@dry_run = dry_run
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(container:, call:)
|
|
29
|
-
raise UsageError.new("from_prefix and to_prefix required") if @from_prefix.nil? || @to_prefix.nil?
|
|
19
|
+
def self.call(container:, call:, from_prefix:, to_prefix:, dry_run: false)
|
|
20
|
+
return Failure(code: :usage_error, message: "from_prefix and to_prefix required") if from_prefix.nil? || to_prefix.nil?
|
|
30
21
|
|
|
31
|
-
leaves = Textus::Action::List.
|
|
32
|
-
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
22
|
+
leaves = Textus::Action::List.leaf_keys(container: container, prefix: from_prefix)
|
|
33
23
|
|
|
34
|
-
if leaves.include?(
|
|
35
|
-
|
|
24
|
+
if leaves.include?(from_prefix)
|
|
25
|
+
return Failure(code: :usage_error,
|
|
26
|
+
message: "from_prefix '#{from_prefix}' is itself a leaf — use `mv` to rename a single key")
|
|
36
27
|
end
|
|
37
28
|
|
|
38
29
|
warnings = []
|
|
39
|
-
warnings << "no keys under #{
|
|
30
|
+
warnings << "no keys under #{from_prefix}" if leaves.empty?
|
|
40
31
|
|
|
41
32
|
steps = leaves.map do |old_key|
|
|
42
|
-
tail = old_key.delete_prefix("#{
|
|
43
|
-
new_key = "#{
|
|
33
|
+
tail = old_key.delete_prefix("#{from_prefix}.")
|
|
34
|
+
new_key = "#{to_prefix}.#{tail}"
|
|
44
35
|
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
45
36
|
end
|
|
46
37
|
|
|
47
|
-
plan = Textus::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
48
|
-
return plan if
|
|
38
|
+
plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
39
|
+
return Success(plan) if dry_run
|
|
49
40
|
|
|
50
41
|
steps.each do |step|
|
|
51
|
-
Textus::Action::KeyMv.
|
|
42
|
+
Value::Result.unwrap(Textus::Action::KeyMv.call(container: container, call: call, old_key: step["from"], new_key: step["to"]))
|
|
52
43
|
end
|
|
53
|
-
plan
|
|
44
|
+
Success(plan)
|
|
54
45
|
end
|
|
55
46
|
end
|
|
56
47
|
end
|
data/lib/textus/action/list.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
5
|
class List < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
6
|
verb :list
|
|
9
7
|
summary "List keys filtered by lane and/or prefix."
|
|
10
8
|
surfaces :cli, :mcp
|
|
@@ -14,28 +12,16 @@ module Textus
|
|
|
14
12
|
description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
|
|
15
13
|
view(:cli) { |rows| { "entries" => rows } }
|
|
16
14
|
|
|
17
|
-
def
|
|
18
|
-
super()
|
|
19
|
-
@prefix = prefix
|
|
20
|
-
@lane = lane
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
def self.call(container:, call: nil, prefix: nil, lane: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
24
16
|
manifest = container.manifest
|
|
25
|
-
rows = manifest.resolver.enumerate(prefix:
|
|
26
|
-
rows = rows.select { |row| row[:manifest_entry].lane ==
|
|
27
|
-
rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } }
|
|
17
|
+
rows = manifest.resolver.enumerate(prefix: prefix)
|
|
18
|
+
rows = rows.select { |row| row[:manifest_entry].lane == lane } if lane
|
|
19
|
+
Success(rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } })
|
|
28
20
|
end
|
|
29
21
|
|
|
30
|
-
def self.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
call_spec = instance_method(:initialize).parameters
|
|
34
|
-
required = call_spec.slice(:keyreq).map(&:last)
|
|
35
|
-
optional = call_spec.slice(:key).map(&:last)
|
|
36
|
-
positional = required + optional
|
|
37
|
-
mapped = positional.zip(args).to_h
|
|
38
|
-
super(**mapped.merge(kwargs))
|
|
22
|
+
def self.leaf_keys(container:, prefix: nil, lane: nil)
|
|
23
|
+
rows = Value::Result.unwrap(call(container: container, prefix: prefix, lane: lane))
|
|
24
|
+
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
39
25
|
end
|
|
40
26
|
end
|
|
41
27
|
end
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
|
-
class Propose <
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
5
|
+
class Propose < Base
|
|
8
6
|
verb :propose
|
|
9
7
|
summary "Write a proposal to the role's propose_lane. Auto-prefixes the key."
|
|
10
8
|
surfaces :cli, :mcp
|
|
@@ -19,33 +17,25 @@ module Textus
|
|
|
19
17
|
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
20
18
|
view { |env, _i| env.to_h_for_wire }
|
|
21
19
|
|
|
22
|
-
def
|
|
23
|
-
super()
|
|
24
|
-
@key = key
|
|
25
|
-
@meta = meta
|
|
26
|
-
@body = body
|
|
27
|
-
@content = content
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def call(container:, call:)
|
|
20
|
+
def self.call(container:, call:, key:, meta: nil, body: nil, content: nil)
|
|
31
21
|
zone = container.manifest.policy.propose_lane_for(call.role)
|
|
32
22
|
unless zone
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
details: { "role" => call.role },
|
|
37
|
-
hint: "the manifest must define a queue zone and '#{call.role}' must hold the 'propose' capability",
|
|
38
|
-
)
|
|
23
|
+
return Failure(code: :propose_forbidden,
|
|
24
|
+
message: "role '#{call.role}' has no writable propose_lane",
|
|
25
|
+
details: { "role" => call.role })
|
|
39
26
|
end
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
mentry = container.manifest.resolver.resolve("#{zone}.#{key}").entry
|
|
29
|
+
Success(container.compositor.write(
|
|
30
|
+
"#{zone}.#{key}",
|
|
31
|
+
mentry: mentry,
|
|
32
|
+
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
33
|
+
meta: meta || {},
|
|
34
|
+
body: body,
|
|
35
|
+
content: content,
|
|
36
|
+
),
|
|
37
|
+
call: call,
|
|
38
|
+
))
|
|
49
39
|
end
|
|
50
40
|
end
|
|
51
41
|
end
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
5
|
class Published < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
6
|
verb :published
|
|
9
7
|
summary "List all entries that declare a publish_to target."
|
|
10
8
|
surfaces :cli
|
|
@@ -14,10 +12,10 @@ module Textus
|
|
|
14
12
|
{}
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
def call(container:, **)
|
|
18
|
-
container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
15
|
+
def self.call(container:, **)
|
|
16
|
+
Success(container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
19
17
|
{ "key" => entry.key, "publish_to" => entry.publish_to }
|
|
20
|
-
end
|
|
18
|
+
end)
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
end
|
data/lib/textus/action/pulse.rb
CHANGED
|
@@ -5,47 +5,40 @@ require "time"
|
|
|
5
5
|
module Textus
|
|
6
6
|
module Action
|
|
7
7
|
class Pulse < Base
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
8
|
verb :pulse
|
|
11
9
|
summary "Delta since cursor — changed entries, pending proposals, index freshness."
|
|
12
10
|
surfaces :cli, :mcp
|
|
13
|
-
around :cursor
|
|
14
11
|
arg :since, Integer, session_default: :cursor,
|
|
15
12
|
description: "audit seq to diff from; defaults to the session cursor"
|
|
16
13
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
def self.call(container:, call:, since: nil, **)
|
|
15
|
+
manifest = container.manifest
|
|
16
|
+
audit_log = container.audit_log
|
|
17
|
+
root = container.root
|
|
18
|
+
since ||= Textus::Store::Cursor.new(root: root, role: call.role).read
|
|
19
|
+
|
|
20
|
+
changed = Value::Result.unwrap(Textus::Action::Audit.call(container: container, seq_since: since))
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@root = container.root
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
"cursor" => @audit_log.latest_seq,
|
|
31
|
-
"changed" => Textus::Action::Audit.new(seq_since: @since).call(container: container),
|
|
32
|
-
"pending_review" => review_keys,
|
|
33
|
-
"contract_etag" => Textus::Etag.for_contract(@root),
|
|
22
|
+
result = {
|
|
23
|
+
"cursor" => audit_log.latest_seq,
|
|
24
|
+
"changed" => changed,
|
|
25
|
+
"pending_review" => review_keys(manifest, container),
|
|
26
|
+
"contract_etag" => Textus::Value::Etag.for_contract(root),
|
|
34
27
|
"index_etag" => index_etag(container),
|
|
35
28
|
}
|
|
36
|
-
end
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
Textus::Store::Cursor.new(root: root, role: call.role).write(result["cursor"])
|
|
31
|
+
Success(result)
|
|
32
|
+
end
|
|
39
33
|
|
|
40
|
-
def review_keys
|
|
41
|
-
queue =
|
|
34
|
+
def self.review_keys(manifest, container)
|
|
35
|
+
queue = manifest.policy.queue_lane
|
|
42
36
|
return [] unless queue
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
38
|
+
Textus::Action::List.leaf_keys(container: container, lane: queue)
|
|
46
39
|
end
|
|
47
40
|
|
|
48
|
-
def index_etag(container)
|
|
41
|
+
def self.index_etag(container)
|
|
49
42
|
path = container.manifest.resolver.resolve("artifacts.system.index").path
|
|
50
43
|
File.exist?(path) ? container.file_store.etag(path) : nil
|
|
51
44
|
rescue Textus::Error
|
data/lib/textus/action/put.rb
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
|
-
class Put <
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
5
|
+
class Put < Base
|
|
8
6
|
verb :put
|
|
9
7
|
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
10
8
|
surfaces :cli, :mcp
|
|
@@ -20,32 +18,20 @@ module Textus
|
|
|
20
18
|
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
21
19
|
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
22
20
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@key,
|
|
38
|
-
mentry: mentry,
|
|
39
|
-
payload: Textus::Envelope::Writer::Payload.new(
|
|
40
|
-
meta: @meta,
|
|
41
|
-
body: @body,
|
|
42
|
-
content: @content,
|
|
43
|
-
),
|
|
44
|
-
if_etag: @if_etag,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
envelope
|
|
48
|
-
end
|
|
21
|
+
def self.call(container:, call:, key:, meta: nil, body: nil, content: nil, if_etag: nil) # rubocop:disable Metrics/ParameterLists
|
|
22
|
+
Textus::Manifest::Data.validate_key!(key)
|
|
23
|
+
mentry = container.manifest.resolver.resolve(key).entry
|
|
24
|
+
Success(container.compositor.write(
|
|
25
|
+
key,
|
|
26
|
+
mentry: mentry,
|
|
27
|
+
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
28
|
+
meta: meta,
|
|
29
|
+
body: body,
|
|
30
|
+
content: content,
|
|
31
|
+
),
|
|
32
|
+
if_etag: if_etag,
|
|
33
|
+
call: call,
|
|
34
|
+
))
|
|
49
35
|
end
|
|
50
36
|
end
|
|
51
37
|
end
|
data/lib/textus/action/rdeps.rb
CHANGED
|
@@ -3,36 +3,21 @@
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
5
|
class Rdeps < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
6
|
verb :rdeps
|
|
9
7
|
summary "List the derived entries that depend on a key (reverse deps / impact set)."
|
|
10
8
|
surfaces :cli, :mcp
|
|
11
9
|
arg :key, String, required: true, positional: true,
|
|
12
10
|
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
13
11
|
|
|
14
|
-
def
|
|
15
|
-
super()
|
|
16
|
-
@key = key
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(container:, **)
|
|
12
|
+
def self.call(container:, key:, **)
|
|
20
13
|
manifest = container.manifest
|
|
21
14
|
rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
|
|
22
15
|
next unless entry.external?
|
|
23
16
|
|
|
24
17
|
sources = Array(entry.source&.sources).compact
|
|
25
|
-
acc << entry.key if sources.any? { |source| source ==
|
|
18
|
+
acc << entry.key if sources.any? { |source| source == key || key.start_with?("#{source}.") }
|
|
26
19
|
end
|
|
27
|
-
{ "key" =>
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.new(*args, **kwargs)
|
|
31
|
-
return super(**kwargs) unless args.any?
|
|
32
|
-
|
|
33
|
-
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
34
|
-
mapped = positional.zip(args).to_h
|
|
35
|
-
super(**mapped.merge(kwargs))
|
|
20
|
+
Success({ "key" => key, "rdeps" => rdeps })
|
|
36
21
|
end
|
|
37
22
|
end
|
|
38
23
|
end
|
data/lib/textus/action/reject.rb
CHANGED
|
@@ -2,35 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Action
|
|
5
|
-
class Reject <
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
5
|
+
class Reject < Base
|
|
8
6
|
verb :reject
|
|
9
7
|
summary "discard a queued proposal without applying it"
|
|
10
8
|
surfaces :cli, :mcp
|
|
11
9
|
cli "reject"
|
|
12
10
|
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
11
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def call(container:, call:)
|
|
20
|
-
run_with_cascade(@pending_key, container:, call:) do
|
|
21
|
-
mentry = container.manifest.resolver.resolve(@pending_key).entry
|
|
22
|
-
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
23
|
-
raise ProposalError.new("reject: '#{@pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
env = Textus::Action::Get.new(key: @pending_key).call(container: container, call: call)
|
|
27
|
-
proposal = env.meta&.dig("proposal") or raise ProposalError.new("entry has no proposal block: #{@pending_key}")
|
|
28
|
-
target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
12
|
+
def self.call(container:, call:, pending_key:)
|
|
13
|
+
mentry = container.manifest.resolver.resolve(pending_key).entry
|
|
14
|
+
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
15
|
+
return Failure(code: :proposal_error, message: "reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
16
|
+
end
|
|
29
17
|
|
|
30
|
-
|
|
18
|
+
env = container.compositor.read(pending_key)
|
|
19
|
+
parsed = proposal_from(env, key: pending_key)
|
|
20
|
+
return parsed if parsed.is_a?(Dry::Monads::Result::Failure)
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
target_key = parsed[:target_key]
|
|
23
|
+
container.compositor.delete(pending_key, mentry: mentry, call: call)
|
|
24
|
+
Success("protocol" => Textus::PROTOCOL, "rejected" => pending_key, "target_key" => target_key)
|
|
34
25
|
end
|
|
35
26
|
end
|
|
36
27
|
end
|