textus 0.12.1 → 0.14.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +214 -0
  4. data/README.md +6 -12
  5. data/SPEC.md +4 -1
  6. data/docs/conventions.md +8 -8
  7. data/lib/textus/application/context.rb +4 -0
  8. data/lib/textus/application/reads/blame.rb +1 -1
  9. data/lib/textus/application/reads/deps.rb +15 -0
  10. data/lib/textus/application/reads/freshness.rb +2 -2
  11. data/lib/textus/application/reads/get.rb +8 -11
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/published.rb +15 -0
  14. data/lib/textus/application/reads/rdeps.rb +15 -0
  15. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  16. data/lib/textus/application/reads/stale.rb +15 -0
  17. data/lib/textus/application/reads/uid.rb +15 -0
  18. data/lib/textus/application/reads/validate_all.rb +15 -0
  19. data/lib/textus/application/reads/where.rb +15 -0
  20. data/lib/textus/application/refresh/all.rb +2 -2
  21. data/lib/textus/application/refresh/worker.rb +3 -3
  22. data/lib/textus/application/writes/accept.rb +7 -7
  23. data/lib/textus/application/writes/build.rb +10 -47
  24. data/lib/textus/application/writes/mv.rb +144 -0
  25. data/lib/textus/application/writes/publish.rb +41 -9
  26. data/lib/textus/application/writes/reject.rb +37 -0
  27. data/lib/textus/cli/verb/accept.rb +1 -2
  28. data/lib/textus/cli/verb/audit.rb +3 -3
  29. data/lib/textus/cli/verb/blame.rb +1 -2
  30. data/lib/textus/cli/verb/build.rb +6 -2
  31. data/lib/textus/cli/verb/delete.rb +1 -2
  32. data/lib/textus/cli/verb/deps.rb +1 -1
  33. data/lib/textus/cli/verb/freshness.rb +1 -2
  34. data/lib/textus/cli/verb/get.rb +2 -3
  35. data/lib/textus/cli/verb/list.rb +1 -1
  36. data/lib/textus/cli/verb/mv.rb +1 -1
  37. data/lib/textus/cli/verb/published.rb +1 -1
  38. data/lib/textus/cli/verb/put.rb +2 -2
  39. data/lib/textus/cli/verb/rdeps.rb +1 -1
  40. data/lib/textus/cli/verb/refresh.rb +1 -2
  41. data/lib/textus/cli/verb/reject.rb +1 -1
  42. data/lib/textus/cli/verb/rule_explain.rb +1 -2
  43. data/lib/textus/cli/verb/schema.rb +1 -1
  44. data/lib/textus/cli/verb/uid.rb +1 -1
  45. data/lib/textus/cli/verb/where.rb +1 -1
  46. data/lib/textus/cli/verb.rb +6 -1
  47. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  48. data/lib/textus/doctor.rb +1 -1
  49. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  50. data/lib/textus/domain/policy/predicates/schema_valid.rb +3 -3
  51. data/lib/textus/entry/base.rb +28 -0
  52. data/lib/textus/entry/json.rb +59 -0
  53. data/lib/textus/entry/markdown.rb +46 -0
  54. data/lib/textus/entry/text.rb +35 -0
  55. data/lib/textus/entry/yaml.rb +59 -0
  56. data/lib/textus/entry.rb +16 -0
  57. data/lib/textus/envelope.rb +44 -14
  58. data/lib/textus/intro.rb +56 -0
  59. data/lib/textus/manifest/entry/parser.rb +84 -0
  60. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  61. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  62. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  63. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  64. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  65. data/lib/textus/manifest/entry/validators.rb +20 -0
  66. data/lib/textus/manifest/entry.rb +35 -213
  67. data/lib/textus/manifest.rb +6 -16
  68. data/lib/textus/operations/reads.rb +39 -0
  69. data/lib/textus/operations/refresh.rb +27 -0
  70. data/lib/textus/operations/writes.rb +21 -0
  71. data/lib/textus/operations.rb +44 -0
  72. data/lib/textus/projection.rb +5 -4
  73. data/lib/textus/refresh.rb +3 -4
  74. data/lib/textus/schema/tools.rb +8 -7
  75. data/lib/textus/store/reader.rb +1 -1
  76. data/lib/textus/store/validator.rb +3 -3
  77. data/lib/textus/store/writer.rb +5 -74
  78. data/lib/textus/store.rb +1 -55
  79. data/lib/textus/version.rb +1 -1
  80. metadata +23 -4
  81. data/lib/textus/composition.rb +0 -72
  82. data/lib/textus/proposal.rb +0 -10
  83. data/lib/textus/store/mover.rb +0 -167
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Uid
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.uid(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class ValidateAll
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call
10
+ @ctx.store.reader.validate_all
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Textus
2
+ module Application
3
+ module Reads
4
+ class Where
5
+ def initialize(ctx:)
6
+ @ctx = ctx
7
+ end
8
+
9
+ def call(key)
10
+ @ctx.store.reader.where(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,9 +5,9 @@ module Textus
5
5
  module_function
6
6
 
7
7
  def call(ctx, prefix: nil, zone: nil)
8
- worker = Textus::Composition.refresh_worker(ctx)
8
+ worker = Textus::Application::Refresh::Worker.new(ctx: ctx, bus: ctx.store.bus)
9
9
 
10
- stale_rows = ctx.store.stale(prefix: prefix, zone: zone)
10
+ stale_rows = Textus::Application::Reads::Stale.new(ctx: ctx).call(prefix: prefix, zone: zone)
11
11
  refreshed = []
12
12
  failed = []
13
13
  skipped = []
@@ -54,10 +54,10 @@ module Textus
54
54
 
55
55
  def persist_and_notify(key, mentry, result, before_etag)
56
56
  normalized = Textus::Refresh.normalize_action_result(result, format: mentry.format)
57
- envelope = @ctx.store.put(
57
+ envelope = Textus::Application::Writes::Put.new(ctx: @ctx, bus: @bus).call(
58
58
  key,
59
59
  meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
60
- as: @ctx.role, suppress_events: true
60
+ suppress_events: true
61
61
  )
62
62
  change = detect_change(before_etag, envelope)
63
63
  unless change == :unchanged
@@ -69,7 +69,7 @@ module Textus
69
69
 
70
70
  def detect_change(before_etag, envelope)
71
71
  if before_etag.nil? then :created
72
- elsif envelope["etag"] == before_etag then :unchanged
72
+ elsif envelope.etag == before_etag then :unchanged
73
73
  else :updated
74
74
  end
75
75
  end
@@ -10,8 +10,8 @@ module Textus
10
10
  def call(pending_key)
11
11
  raise ProposalError.new("only human role can accept proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
12
12
 
13
- env = @ctx.store.get(pending_key)
14
- proposal = env["_meta"]["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
13
+ env = @ctx.store.reader.get(pending_key)
14
+ proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
15
15
  target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
16
16
  action = proposal["action"] || "put"
17
17
 
@@ -21,16 +21,16 @@ module Textus
21
21
  when "put"
22
22
  # Nested proposal "frontmatter" — the meta to write to the accepted
23
23
  # target. Not related to the removed intake-handler legacy bridge.
24
- target_meta = env["_meta"]["frontmatter"] || {}
25
- target_body = env["body"]
26
- Composition.writes_put(@ctx).call(target, meta: target_meta, body: target_body)
24
+ target_meta = env.meta["frontmatter"] || {}
25
+ target_body = env.body
26
+ Textus::Application::Writes::Put.new(ctx: @ctx, bus: @bus).call(target, meta: target_meta, body: target_body)
27
27
  when "delete"
28
- Composition.writes_delete(@ctx).call(target)
28
+ Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(target)
29
29
  else
30
30
  raise ProposalError.new("unknown action: #{action}")
31
31
  end
32
32
 
33
- Composition.writes_delete(@ctx).call(pending_key)
33
+ Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(pending_key)
34
34
 
35
35
  @bus.publish(:proposal_accepted,
36
36
  store: @ctx.with_role(@ctx.role),
@@ -4,9 +4,12 @@ module Textus
4
4
  module Application
5
5
  module Writes
6
6
  # Materializes generator-zone entries (template + projection) onto disk
7
- # and copies the result to any configured `publish_to` / `publish_each`
8
- # targets. Fires `:build_completed` and `:file_published` events on the bus,
9
- # tagged with the request's correlation_id for traceability.
7
+ # and copies the result to any configured `publish_to:` targets. Fires
8
+ # `:build_completed` and `:file_published` events.
9
+ #
10
+ # For `publish_each:` (per-leaf publishing of nested entries), see
11
+ # `Application::Writes::Publish`. The CLI verb `textus build` calls
12
+ # both classes and merges the results.
10
13
  class Build
11
14
  def initialize(ctx:, bus:)
12
15
  @ctx = ctx
@@ -14,16 +17,14 @@ module Textus
14
17
  end
15
18
 
16
19
  def call(prefix: nil)
17
- built = []
18
- manifest.entries.each do |mentry|
20
+ built = manifest.entries.filter_map do |mentry|
19
21
  next unless mentry.in_generator_zone?
20
22
  next unless mentry.projection || mentry.template
21
23
  next if prefix && !mentry.key.start_with?(prefix)
22
24
 
23
- built << materialize(mentry)
25
+ materialize(mentry)
24
26
  end
25
- published_leaves = publish_leaves(prefix: prefix)
26
- { "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
27
+ { "protocol" => Textus::PROTOCOL, "built" => built }
27
28
  end
28
29
 
29
30
  private
@@ -32,41 +33,6 @@ module Textus
32
33
  def manifest = store.manifest
33
34
  def root = store.root
34
35
 
35
- def publish_leaves(prefix: nil)
36
- repo_root = File.dirname(root)
37
- out = []
38
- manifest.entries.each do |mentry|
39
- next unless mentry.nested && mentry.publish_each
40
- next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
41
-
42
- manifest.enumerate(prefix: mentry.key).each do |row|
43
- next unless row[:manifest_entry].equal?(mentry)
44
- next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
45
-
46
- out << publish_leaf(mentry, row, repo_root)
47
- end
48
- end
49
- out
50
- end
51
-
52
- def publish_leaf(mentry, row, repo_root)
53
- target_rel = mentry.publish_target_for(row[:key])
54
- target_abs = File.expand_path(File.join(repo_root, target_rel))
55
- unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
56
- raise PublishError.new(
57
- "entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
58
- )
59
- end
60
-
61
- Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: root)
62
- publish_event(:file_published,
63
- key: row[:key],
64
- envelope: store.get(row[:key]),
65
- source: row[:path],
66
- target: target_abs)
67
- { "key" => row[:key], "source" => row[:path], "target" => target_abs }
68
- end
69
-
70
36
  def materialize(mentry)
71
37
  target_path = Builder::Pipeline.run(
72
38
  store: store,
@@ -85,7 +51,7 @@ module Textus
85
51
  end
86
52
 
87
53
  def publish_and_fire(mentry, target_path)
88
- envelope = store.get(mentry.key)
54
+ envelope = store.reader.get(mentry.key)
89
55
  repo_root = File.dirname(root)
90
56
 
91
57
  mentry.publish_to.each do |rel|
@@ -105,9 +71,6 @@ module Textus
105
71
  end
106
72
 
107
73
  def publish_event(event, **payload)
108
- # `with_role` returns a Context that preserves the original
109
- # correlation_id, so hooks reading `store.correlation_id` see the
110
- # same value as the event's top-level correlation_id key.
111
74
  @bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
112
75
  end
113
76
  end
@@ -0,0 +1,144 @@
1
+ require "fileutils"
2
+
3
+ module Textus
4
+ module Application
5
+ module Writes
6
+ class Mv
7
+ MovePlan = Data.define(
8
+ :old_key, :new_key, :old_path, :new_path,
9
+ :new_mentry, :uid, :etag_before
10
+ )
11
+
12
+ def initialize(ctx:, bus:)
13
+ @ctx = ctx
14
+ @bus = bus
15
+ end
16
+
17
+ def call(old_key, new_key, dry_run: false)
18
+ plan, pre_env = prepare_plan(old_key, new_key)
19
+ return dry_run_result(plan) if dry_run
20
+
21
+ plan = ensure_uid!(plan, pre_env: pre_env)
22
+ etag_after = perform_move!(plan)
23
+ new_envelope = record_move(plan, etag_after: etag_after)
24
+ success_result(plan, new_envelope: new_envelope)
25
+ end
26
+
27
+ private
28
+
29
+ def manifest = @ctx.store.manifest
30
+ def reader = @ctx.store.reader
31
+
32
+ def prepare_plan(old_key, new_key)
33
+ manifest.validate_key!(old_key)
34
+ manifest.validate_key!(new_key)
35
+ raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
36
+
37
+ old_mentry, old_path, = manifest.resolve(old_key)
38
+ raise UnknownKey.new(old_key) unless File.exist?(old_path)
39
+
40
+ new_mentry, new_path, = manifest.resolve(new_key)
41
+ validate_zone_and_format!(old_mentry, new_mentry)
42
+ validate_writer!(old_mentry, old_key)
43
+ raise UsageError.new("mv: target '#{new_key}' already exists at #{new_path}") if File.exist?(new_path)
44
+
45
+ pre_env = reader.get(old_key)
46
+ plan = MovePlan.new(
47
+ old_key: old_key, new_key: new_key,
48
+ old_path: old_path, new_path: new_path,
49
+ new_mentry: new_mentry,
50
+ uid: pre_env.uid, etag_before: pre_env.etag
51
+ )
52
+ [plan, pre_env]
53
+ end
54
+
55
+ def validate_zone_and_format!(old_mentry, new_mentry)
56
+ if old_mentry.zone != new_mentry.zone
57
+ raise UsageError.new(
58
+ "mv: cross-zone move refused (#{old_mentry.zone} → #{new_mentry.zone}). " \
59
+ "Use put+delete for cross-zone moves.",
60
+ )
61
+ end
62
+ return if old_mentry.format == new_mentry.format
63
+
64
+ raise UsageError.new("mv: format mismatch (#{old_mentry.format} → #{new_mentry.format}); refusing.")
65
+ end
66
+
67
+ def validate_writer!(mentry, key)
68
+ writers = manifest.zone_writers(mentry.zone)
69
+ return if writers.include?(@ctx.role)
70
+
71
+ raise WriteForbidden.new(key, mentry.zone, writers: writers)
72
+ end
73
+
74
+ def ensure_uid!(plan, pre_env:)
75
+ return plan if plan.uid
76
+
77
+ env = Textus::Application::Writes::Put.new(ctx: @ctx, bus: @bus).call(
78
+ plan.old_key,
79
+ meta: pre_env.meta,
80
+ body: pre_env.body,
81
+ content: pre_env.content,
82
+ suppress_events: true,
83
+ )
84
+ plan.with(uid: env.uid, etag_before: env.etag)
85
+ end
86
+
87
+ def perform_move!(plan)
88
+ FileUtils.mkdir_p(File.dirname(plan.new_path))
89
+ FileUtils.mv(plan.old_path, plan.new_path)
90
+ rewrite_name_for_mv!(plan.new_mentry, plan.new_path, plan.new_key)
91
+ Etag.for_file(plan.new_path)
92
+ end
93
+
94
+ def record_move(plan, etag_after:)
95
+ extras = {
96
+ "from_key" => plan.old_key, "to_key" => plan.new_key,
97
+ "from_path" => plan.old_path, "to_path" => plan.new_path,
98
+ "uid" => plan.uid
99
+ }
100
+ extras["correlation_id"] = @ctx.correlation_id if @ctx.correlation_id
101
+
102
+ @ctx.store.audit_log.append(
103
+ role: @ctx.role, verb: "mv", key: plan.new_key,
104
+ etag_before: plan.etag_before, etag_after: etag_after,
105
+ extras: extras
106
+ )
107
+ new_envelope = reader.get(plan.new_key)
108
+ @bus.publish(:entry_renamed,
109
+ store: @ctx.with_role(@ctx.role),
110
+ key: plan.new_key,
111
+ from_key: plan.old_key,
112
+ to_key: plan.new_key,
113
+ envelope: new_envelope,
114
+ correlation_id: @ctx.correlation_id)
115
+ new_envelope
116
+ end
117
+
118
+ def dry_run_result(plan)
119
+ {
120
+ "protocol" => PROTOCOL, "ok" => true, "dry_run" => true,
121
+ "from_key" => plan.old_key, "to_key" => plan.new_key,
122
+ "from_path" => plan.old_path, "to_path" => plan.new_path,
123
+ "uid" => plan.uid
124
+ }
125
+ end
126
+
127
+ def success_result(plan, new_envelope:)
128
+ {
129
+ "protocol" => PROTOCOL, "ok" => true,
130
+ "from_key" => plan.old_key, "to_key" => plan.new_key,
131
+ "from_path" => plan.old_path, "to_path" => plan.new_path,
132
+ "uid" => plan.uid,
133
+ "envelope" => new_envelope.to_h_for_wire
134
+ }
135
+ end
136
+
137
+ def rewrite_name_for_mv!(mentry, new_path, new_key)
138
+ basename = new_key.split(".").last
139
+ Entry.for_format(mentry.format).rewrite_name(new_path, basename)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,23 +1,55 @@
1
1
  module Textus
2
2
  module Application
3
3
  module Writes
4
+ # Copies nested-leaf entries to their `publish_each:` targets. Fires
5
+ # `:file_published` for each copy. Mirror of `Build` for the publish
6
+ # half — split out from the old Build per ADR 0007.
4
7
  class Publish
5
8
  def initialize(ctx:, bus:)
6
9
  @ctx = ctx
7
10
  @bus = bus
8
11
  end
9
12
 
10
- def call(source:, target:, key:)
11
- Textus::Infra::Publisher.publish(
12
- source: source,
13
- target: target,
14
- store_root: @ctx.store.root,
15
- )
13
+ def call(prefix: nil)
14
+ repo_root = File.dirname(store.root)
15
+ out = []
16
+ manifest.entries.each do |mentry|
17
+ next unless mentry.nested && mentry.publish_each
18
+ next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
19
+
20
+ manifest.enumerate(prefix: mentry.key).each do |row|
21
+ next unless row[:manifest_entry].equal?(mentry)
22
+ next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
23
+
24
+ out << publish_leaf(mentry, row, repo_root)
25
+ end
26
+ end
27
+ { "protocol" => Textus::PROTOCOL, "published_leaves" => out }
28
+ end
29
+
30
+ private
31
+
32
+ def store = @ctx.store
33
+ def manifest = store.manifest
34
+
35
+ def publish_leaf(mentry, row, repo_root)
36
+ target_rel = mentry.publish_target_for(row[:key])
37
+ target_abs = File.expand_path(File.join(repo_root, target_rel))
38
+ unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
39
+ raise PublishError.new(
40
+ "entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
41
+ )
42
+ end
43
+
44
+ Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: store.root)
16
45
  @bus.publish(:file_published,
17
- key: key,
18
- source: source,
19
- target: target,
46
+ store: @ctx.with_role(@ctx.role),
47
+ key: row[:key],
48
+ envelope: store.reader.get(row[:key]),
49
+ source: row[:path],
50
+ target: target_abs,
20
51
  correlation_id: @ctx.correlation_id)
52
+ { "key" => row[:key], "source" => row[:path], "target" => target_abs }
21
53
  end
22
54
  end
23
55
  end
@@ -0,0 +1,37 @@
1
+ module Textus
2
+ module Application
3
+ module Writes
4
+ class Reject
5
+ def initialize(ctx:, bus:)
6
+ @ctx = ctx
7
+ @bus = bus
8
+ end
9
+
10
+ def call(pending_key)
11
+ raise ProposalError.new("only human role can reject proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
12
+
13
+ mentry, = @ctx.store.manifest.resolve(pending_key)
14
+ unless mentry.in_proposal_zone?
15
+ raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
16
+ end
17
+
18
+ env = @ctx.store.reader.get(pending_key)
19
+ proposal = env.meta&.dig("proposal") or
20
+ raise ProposalError.new("entry has no proposal block: #{pending_key}")
21
+ target_key = proposal["target_key"] or
22
+ raise ProposalError.new("proposal missing target_key")
23
+
24
+ Textus::Application::Writes::Delete.new(ctx: @ctx, bus: @bus).call(pending_key, suppress_events: true)
25
+
26
+ @bus.publish(:proposal_rejected,
27
+ store: @ctx.with_role(@ctx.role),
28
+ key: pending_key,
29
+ target_key: target_key,
30
+ correlation_id: @ctx.correlation_id)
31
+
32
+ { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -6,8 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("accept requires a key")
9
- ctx = context_for(store)
10
- emit(Textus::Composition.writes_accept(ctx).call(key))
9
+ emit(operations_for(store).writes.accept.call(key))
11
10
  end
12
11
  end
13
12
  end
@@ -11,9 +11,9 @@ module Textus
11
11
  option :limit, "--limit=N"
12
12
 
13
13
  def call(store)
14
- ctx = context_for(store)
15
- since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ctx.now)
16
- rows = Textus::Composition.audit(ctx).call(
14
+ ops = operations_for(store)
15
+ since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ops.ctx.now)
16
+ rows = ops.reads.audit.call(
17
17
  key: key_filter,
18
18
  zone: zone,
19
19
  role: role_filter,
@@ -6,8 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("blame requires a key")
9
- ctx = context_for(store)
10
- rows = Textus::Composition.blame(ctx).call(key: key, limit: limit&.to_i)
9
+ rows = operations_for(store).reads.blame.call(key: key, limit: limit&.to_i)
11
10
  emit({ "verb" => "blame", "key" => key, "rows" => rows })
12
11
  end
13
12
  end
@@ -5,8 +5,12 @@ module Textus
5
5
  option :prefix, "--prefix=K"
6
6
 
7
7
  def call(store)
8
- ctx = Textus::Composition.context(store, role: "builder")
9
- emit(Textus::Composition.writes_build(ctx).call(prefix: prefix))
8
+ ops = Textus::Operations.for(store, role: "builder")
9
+ build_res = ops.writes.build.call(prefix: prefix)
10
+ publish_res = ops.writes.publish.call(prefix: prefix)
11
+ emit({ "protocol" => Textus::PROTOCOL,
12
+ "built" => build_res["built"],
13
+ "published_leaves" => publish_res["published_leaves"] })
10
14
  end
11
15
  end
12
16
  end
@@ -7,8 +7,7 @@ module Textus
7
7
 
8
8
  def call(store)
9
9
  key = positional.shift or raise UsageError.new("delete requires a key")
10
- ctx = context_for(store)
11
- emit(Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag))
10
+ emit(operations_for(store).writes.delete.call(key, if_etag: if_etag))
12
11
  end
13
12
  end
14
13
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class Deps < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("deps requires a key")
7
- emit({ "key" => key, "deps" => store.deps(key) })
7
+ emit({ "key" => key, "deps" => operations_for(store).reads.deps.call(key) })
8
8
  end
9
9
  end
10
10
  end
@@ -6,8 +6,7 @@ module Textus
6
6
  option :zone, "--zone=Z"
7
7
 
8
8
  def call(store)
9
- ctx = context_for(store)
10
- rows = Textus::Composition.freshness(ctx).call(prefix: prefix, zone: zone)
9
+ rows = operations_for(store).reads.freshness.call(prefix: prefix, zone: zone)
11
10
  emit({ "verb" => "freshness", "rows" => rows })
12
11
  end
13
12
  end
@@ -6,11 +6,10 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("get requires a key")
9
- ctx = context_for(store)
10
- result = Textus::Composition.reads_get(ctx).call(key)
9
+ result = operations_for(store).reads.get.call(key)
11
10
  raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
12
11
 
13
- emit(result)
12
+ emit(result.to_h_for_wire)
14
13
  end
15
14
  end
16
15
  end
@@ -6,7 +6,7 @@ module Textus
6
6
  option :zone, "--zone=Z"
7
7
 
8
8
  def call(store)
9
- emit({ "entries" => store.list(prefix: prefix, zone: zone) })
9
+ emit({ "entries" => operations_for(store).reads.list.call(prefix: prefix, zone: zone) })
10
10
  end
11
11
  end
12
12
  end
@@ -8,7 +8,7 @@ module Textus
8
8
  def call(store)
9
9
  old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
10
10
  new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
11
- emit(store.mv(old_key, new_key, as: resolved_role(store), dry_run: dry_run || false))
11
+ emit(operations_for(store).writes.mv.call(old_key, new_key, dry_run: dry_run || false))
12
12
  end
13
13
  end
14
14
  end
@@ -3,7 +3,7 @@ module Textus
3
3
  class Verb
4
4
  class Published < Verb
5
5
  def call(store)
6
- emit({ "published" => store.published })
6
+ emit({ "published" => operations_for(store).reads.published.call })
7
7
  end
8
8
  end
9
9
  end
@@ -43,8 +43,8 @@ module Textus
43
43
  meta = payload["_meta"] || {}
44
44
  body = payload["body"] || ""
45
45
  if_etag = payload["if_etag"]
46
- ctx = Textus::Composition.context(store, role: role)
47
- emit(Textus::Composition.writes_put(ctx).call(key, meta: meta, body: body, if_etag: if_etag))
46
+ result = Textus::Operations.for(store, role: role).writes.put.call(key, meta: meta, body: body, if_etag: if_etag)
47
+ emit(result.to_h_for_wire)
48
48
  end
49
49
  end
50
50
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class Rdeps < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("rdeps requires a key")
7
- emit({ "key" => key, "rdeps" => store.rdeps(key) })
7
+ emit({ "key" => key, "rdeps" => operations_for(store).reads.rdeps.call(key) })
8
8
  end
9
9
  end
10
10
  end
@@ -6,8 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("refresh requires a key")
9
- ctx = context_for(store)
10
- emit(Textus::Composition.refresh_worker(ctx).run(key))
9
+ emit(operations_for(store).refresh.worker.run(key).to_h_for_wire)
11
10
  end
12
11
  end
13
12
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("reject requires a key")
9
- emit(store.reject(key, as: resolved_role(store)))
9
+ emit(operations_for(store).writes.reject.call(key))
10
10
  end
11
11
  end
12
12
  end
@@ -4,8 +4,7 @@ module Textus
4
4
  class RuleExplain < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("policy explain requires a KEY")
7
- ctx = context_for(store)
8
- result = Textus::Composition.policy_explain(ctx).call(key: key)
7
+ result = operations_for(store).reads.policy_explain.call(key: key)
9
8
  emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
10
9
  end
11
10
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class Schema < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("schema requires a key")
7
- emit(store.schema_envelope(key))
7
+ emit(operations_for(store).reads.schema_envelope.call(key))
8
8
  end
9
9
  end
10
10
  end