textus 0.10.5 → 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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +60 -40
  3. data/CHANGELOG.md +318 -3
  4. data/README.md +34 -27
  5. data/SPEC.md +226 -145
  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 +4 -4
  11. data/lib/textus/application/reads/get.rb +9 -12
  12. data/lib/textus/application/reads/list.rb +15 -0
  13. data/lib/textus/application/reads/policy_explain.rb +2 -2
  14. data/lib/textus/application/reads/published.rb +15 -0
  15. data/lib/textus/application/reads/rdeps.rb +15 -0
  16. data/lib/textus/application/reads/schema_envelope.rb +15 -0
  17. data/lib/textus/application/reads/stale.rb +15 -0
  18. data/lib/textus/application/reads/uid.rb +15 -0
  19. data/lib/textus/application/reads/validate_all.rb +15 -0
  20. data/lib/textus/application/reads/where.rb +15 -0
  21. data/lib/textus/application/refresh/all.rb +2 -2
  22. data/lib/textus/application/refresh/orchestrator.rb +1 -1
  23. data/lib/textus/application/refresh/worker.rb +8 -8
  24. data/lib/textus/application/writes/accept.rb +26 -8
  25. data/lib/textus/application/writes/build.rb +12 -49
  26. data/lib/textus/application/writes/delete.rb +1 -1
  27. data/lib/textus/application/writes/mv.rb +144 -0
  28. data/lib/textus/application/writes/publish.rb +42 -10
  29. data/lib/textus/application/writes/put.rb +1 -1
  30. data/lib/textus/application/writes/reject.rb +37 -0
  31. data/lib/textus/builder/pipeline.rb +1 -1
  32. data/lib/textus/builder/renderer/json.rb +1 -1
  33. data/lib/textus/builder/renderer/yaml.rb +1 -1
  34. data/lib/textus/cli/group/key.rb +1 -1
  35. data/lib/textus/cli/group/refresh.rb +21 -0
  36. data/lib/textus/cli/group/rule.rb +11 -0
  37. data/lib/textus/cli/verb/accept.rb +1 -2
  38. data/lib/textus/cli/verb/audit.rb +3 -3
  39. data/lib/textus/cli/verb/blame.rb +1 -2
  40. data/lib/textus/cli/verb/build.rb +6 -2
  41. data/lib/textus/cli/verb/delete.rb +1 -2
  42. data/lib/textus/cli/verb/deps.rb +1 -1
  43. data/lib/textus/cli/verb/freshness.rb +1 -2
  44. data/lib/textus/cli/verb/get.rb +2 -3
  45. data/lib/textus/cli/verb/hook_run.rb +3 -2
  46. data/lib/textus/cli/verb/hooks.rb +1 -1
  47. data/lib/textus/cli/verb/{migrate_keys.rb → key_normalize.rb} +1 -1
  48. data/lib/textus/cli/verb/list.rb +1 -1
  49. data/lib/textus/cli/verb/mv.rb +1 -1
  50. data/lib/textus/cli/verb/published.rb +1 -1
  51. data/lib/textus/cli/verb/put.rb +3 -3
  52. data/lib/textus/cli/verb/rdeps.rb +1 -1
  53. data/lib/textus/cli/verb/refresh.rb +1 -2
  54. data/lib/textus/cli/verb/reject.rb +1 -1
  55. data/lib/textus/cli/verb/{policy_explain.rb → rule_explain.rb} +2 -3
  56. data/lib/textus/cli/verb/{policy_list.rb → rule_list.rb} +3 -3
  57. data/lib/textus/cli/verb/schema.rb +1 -1
  58. data/lib/textus/cli/verb/uid.rb +1 -1
  59. data/lib/textus/cli/verb/where.rb +1 -1
  60. data/lib/textus/cli/verb.rb +9 -3
  61. data/lib/textus/cli.rb +6 -6
  62. data/lib/textus/doctor/check/handler_allowlist.rb +1 -1
  63. data/lib/textus/doctor/check/illegal_keys.rb +39 -16
  64. data/lib/textus/doctor/check/intake_registration.rb +4 -4
  65. data/lib/textus/doctor/check/protocol_version.rb +47 -0
  66. data/lib/textus/doctor/check/{policy_ambiguity.rb → rule_ambiguity.rb} +6 -6
  67. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  68. data/lib/textus/doctor.rb +6 -5
  69. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  70. data/lib/textus/domain/permission.rb +4 -4
  71. data/lib/textus/domain/policy/predicates/human_accept.rb +31 -0
  72. data/lib/textus/domain/policy/predicates/schema_valid.rb +50 -0
  73. data/lib/textus/domain/policy/promotion.rb +45 -0
  74. data/lib/textus/entry/base.rb +28 -0
  75. data/lib/textus/entry/json.rb +59 -0
  76. data/lib/textus/entry/markdown.rb +46 -0
  77. data/lib/textus/entry/text.rb +35 -0
  78. data/lib/textus/entry/yaml.rb +59 -0
  79. data/lib/textus/entry.rb +16 -0
  80. data/lib/textus/envelope.rb +44 -14
  81. data/lib/textus/errors.rb +24 -5
  82. data/lib/textus/hooks/builtin.rb +5 -5
  83. data/lib/textus/hooks/dispatcher.rb +1 -1
  84. data/lib/textus/hooks/dsl.rb +3 -10
  85. data/lib/textus/hooks/loader.rb +1 -2
  86. data/lib/textus/hooks/registry.rb +22 -21
  87. data/lib/textus/infra/refresh/detached.rb +1 -1
  88. data/lib/textus/init.rb +25 -34
  89. data/lib/textus/intro.rb +65 -9
  90. data/lib/textus/manifest/entry/parser.rb +84 -0
  91. data/lib/textus/manifest/entry/validators/events.rb +21 -0
  92. data/lib/textus/manifest/entry/validators/format_matrix.rb +26 -0
  93. data/lib/textus/manifest/entry/validators/index_filename.rb +45 -0
  94. data/lib/textus/manifest/entry/validators/inject_intro.rb +18 -0
  95. data/lib/textus/manifest/entry/validators/publish_each.rb +37 -0
  96. data/lib/textus/manifest/entry/validators.rb +20 -0
  97. data/lib/textus/manifest/entry.rb +38 -189
  98. data/lib/textus/manifest/{policies.rb → rules.rb} +12 -10
  99. data/lib/textus/manifest/schema.rb +49 -0
  100. data/lib/textus/manifest.rb +50 -24
  101. data/lib/textus/migrate_keys.rb +1 -1
  102. data/lib/textus/operations/reads.rb +39 -0
  103. data/lib/textus/operations/refresh.rb +27 -0
  104. data/lib/textus/operations/writes.rb +21 -0
  105. data/lib/textus/operations.rb +44 -0
  106. data/lib/textus/projection.rb +9 -8
  107. data/lib/textus/refresh.rb +4 -5
  108. data/lib/textus/schema/tools.rb +8 -7
  109. data/lib/textus/store/reader.rb +1 -1
  110. data/lib/textus/store/staleness/intake_check.rb +1 -1
  111. data/lib/textus/store/validator.rb +3 -3
  112. data/lib/textus/store/writer.rb +5 -74
  113. data/lib/textus/store.rb +1 -55
  114. data/lib/textus/version.rb +2 -2
  115. data/lib/textus.rb +1 -0
  116. metadata +35 -10
  117. data/lib/textus/cli/group/policy.rb +0 -11
  118. data/lib/textus/composition.rb +0 -72
  119. data/lib/textus/proposal.rb +0 -10
  120. data/lib/textus/store/mover.rb +0 -167
@@ -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
- )
16
- @bus.publish(:published,
17
- key: key,
18
- source: source,
19
- target: target,
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)
45
+ @bus.publish(:file_published,
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
@@ -25,7 +25,7 @@ module Textus
25
25
  )
26
26
 
27
27
  unless suppress_events
28
- @bus.publish(:put,
28
+ @bus.publish(:entry_put,
29
29
  store: @ctx.with_role(@ctx.role),
30
30
  key: key,
31
31
  envelope: envelope,
@@ -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
@@ -10,7 +10,7 @@ module Textus
10
10
  from = Array(mentry.projection&.fetch("select", nil)).compact
11
11
  meta["from"] = from unless from.empty?
12
12
  meta["template"] = mentry.template if mentry.template
13
- reduce = mentry.projection&.dig("reduce")
13
+ reduce = mentry.projection&.dig("transform")
14
14
  meta["reduce"] = reduce if reduce
15
15
 
16
16
  out = { "_meta" => meta }
@@ -28,7 +28,7 @@ module Textus
28
28
  end
29
29
 
30
30
  def default_shape(mentry, data)
31
- if mentry.projection && mentry.projection["reduce"] && data.is_a?(Hash) && !data.key?("entries")
31
+ if mentry.projection && mentry.projection["transform"] && data.is_a?(Hash) && !data.key?("entries")
32
32
  data
33
33
  elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
34
34
  { "entries" => data["entries"] }
@@ -28,7 +28,7 @@ module Textus
28
28
  end
29
29
 
30
30
  def default_shape(mentry, data)
31
- if mentry.projection && mentry.projection["reduce"] && data.is_a?(Hash) && !data.key?("entries")
31
+ if mentry.projection && mentry.projection["transform"] && data.is_a?(Hash) && !data.key?("entries")
32
32
  data
33
33
  elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
34
34
  { "entries" => data["entries"] }
@@ -5,7 +5,7 @@ module Textus
5
5
  self.cli_name = "key"
6
6
  subcommands["mv"] = Verb::Mv
7
7
  subcommands["uid"] = Verb::Uid
8
- subcommands["migrate"] = Verb::MigrateKeys
8
+ subcommands["normalize"] = Verb::KeyNormalize
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class Refresh < Group
5
+ self.cli_name = "refresh"
6
+ subcommands["stale"] = Verb::RefreshStale
7
+
8
+ def parse(argv)
9
+ if argv.first == "stale"
10
+ argv.shift
11
+ @sub_klass = Verb::RefreshStale
12
+ else
13
+ @sub_klass = Verb::Refresh
14
+ end
15
+ @sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
16
+ @sub.parse(argv)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class Rule < Group
5
+ self.cli_name = "rule"
6
+ subcommands["list"] = Verb::RuleList
7
+ subcommands["explain"] = Verb::RuleExplain
8
+ end
9
+ end
10
+ end
11
+ 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: "build")
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
@@ -15,7 +15,8 @@ module Textus
15
15
  @raw_argv.each do |tok|
16
16
  case tok
17
17
  when /\A--as=(.+)\z/ then as_flag = ::Regexp.last_match(1)
18
- when /\A--format=/ then next
18
+ when /\A--output=/ then next
19
+ when /\A--format=/ then raise FlagRenamed.new("--format", "--output")
19
20
  when /\A--([\w-]+)=(.*)\z/ then args[::Regexp.last_match(1)] = ::Regexp.last_match(2)
20
21
  else
21
22
  raise UsageError.new("unknown arg to 'hook run #{name}': #{tok}")
@@ -23,7 +24,7 @@ module Textus
23
24
  end
24
25
 
25
26
  role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
26
- callable = store.registry.rpc_callable(:intake, name)
27
+ callable = store.registry.rpc_callable(:resolve_intake, name)
27
28
  view = Application::Context.new(store: store, role: role)
28
29
 
29
30
  begin
@@ -35,7 +35,7 @@ module Textus
35
35
 
36
36
  rows << {
37
37
  "event" => evt.to_s, "mode" => "manifest", "exec" => defn["exec"],
38
- "key" => e.key, "as" => defn["as"] || "script"
38
+ "key" => e.key, "as" => defn["as"] || "runner"
39
39
  }
40
40
  end
41
41
  end
@@ -1,7 +1,7 @@
1
1
  module Textus
2
2
  class CLI
3
3
  class Verb
4
- class MigrateKeys < Verb
4
+ class KeyNormalize < Verb
5
5
  option :write, "--write"
6
6
  option :dry_run, "--dry-run"
7
7
 
@@ -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
@@ -15,7 +15,7 @@ module Textus
15
15
  raw = @stdin.read
16
16
  payload =
17
17
  if fetch_name
18
- callable = store.registry.rpc_callable(:intake, fetch_name)
18
+ callable = store.registry.rpc_callable(:resolve_intake, fetch_name)
19
19
  result =
20
20
  begin
21
21
  Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
@@ -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
@@ -1,11 +1,10 @@
1
1
  module Textus
2
2
  class CLI
3
3
  class Verb
4
- class PolicyExplain < Verb
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
@@ -1,9 +1,9 @@
1
1
  module Textus
2
2
  class CLI
3
3
  class Verb
4
- class PolicyList < Verb
4
+ class RuleList < Verb
5
5
  def call(store)
6
- policies = store.manifest.policies.blocks.map do |b|
6
+ policies = store.manifest.rules.blocks.map do |b|
7
7
  row = { "match" => b.match }
8
8
  if b.refresh
9
9
  row["refresh"] = {
@@ -13,7 +13,7 @@ module Textus
13
13
  }
14
14
  end
15
15
  row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
16
- row["promote_requires"] = b.promote.requires if b.promote
16
+ row["promotion"] = { "requires" => b.promote.requires } if b.promote
17
17
  row["retention"] = b.retention if b.retention
18
18
  row
19
19
  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
@@ -4,7 +4,7 @@ module Textus
4
4
  class Uid < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("uid requires a key")
7
- emit({ "key" => key, "uid" => store.uid(key) })
7
+ emit({ "key" => key, "uid" => operations_for(store).reads.uid.call(key) })
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class Where < Verb
5
5
  def call(store)
6
6
  key = positional.shift or raise UsageError.new("where requires a key")
7
- emit(store.where(key))
7
+ emit(operations_for(store).reads.where.call(key))
8
8
  end
9
9
  end
10
10
  end
@@ -41,9 +41,10 @@ module Textus
41
41
  self.class.options.each do |name, optspec|
42
42
  o.on(optspec) { |v| public_send(:"#{name}=", v) }
43
43
  end
44
- o.on("--format=FMT") { |v| fmt = v }
44
+ o.on("--output=FMT") { |v| fmt = v }
45
+ o.on("--format=FMT") { |_v| raise FlagRenamed.new("--format", "--output") }
45
46
  end.permute!(argv)
46
- raise UsageError.new("only --format=json is supported in v1") unless fmt == "json"
47
+ raise UsageError.new("only --output=json is supported in v1") unless fmt == "json"
47
48
 
48
49
  @positional = argv.dup
49
50
  end
@@ -69,7 +70,12 @@ module Textus
69
70
  # Convenience for verbs whose only pre-call boilerplate is
70
71
  # resolving the role and wrapping it in a context.
71
72
  def context_for(store)
72
- Textus::Composition.context(store, role: resolved_role(store))
73
+ Textus::Operations.for(store, role: resolved_role(store)).ctx
74
+ end
75
+
76
+ # Returns an Operations instance bound to the resolved role.
77
+ def operations_for(store)
78
+ Textus::Operations.for(store, role: resolved_role(store))
73
79
  end
74
80
  end
75
81
  end