textus 0.20.2 → 0.26.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +148 -45
  3. data/CHANGELOG.md +194 -0
  4. data/README.md +8 -5
  5. data/SPEC.md +54 -15
  6. data/docs/conventions.md +10 -0
  7. data/lib/textus/application/caps.rb +49 -0
  8. data/lib/textus/application/context.rb +2 -2
  9. data/lib/textus/application/envelope/reader.rb +44 -0
  10. data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
  11. data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
  12. data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
  13. data/lib/textus/application/maintenance/migrate.rb +59 -0
  14. data/lib/textus/application/maintenance/rule_lint.rb +65 -0
  15. data/lib/textus/application/maintenance/zone_mv.rb +60 -0
  16. data/lib/textus/application/maintenance.rb +17 -0
  17. data/lib/textus/application/projection.rb +12 -10
  18. data/lib/textus/application/read/audit.rb +106 -0
  19. data/lib/textus/application/read/blame.rb +91 -0
  20. data/lib/textus/application/read/deps.rb +34 -0
  21. data/lib/textus/application/read/freshness.rb +110 -0
  22. data/lib/textus/application/read/get.rb +75 -0
  23. data/lib/textus/application/read/get_or_refresh.rb +63 -0
  24. data/lib/textus/application/read/list.rb +25 -0
  25. data/lib/textus/application/read/policy_explain.rb +47 -0
  26. data/lib/textus/application/read/published.rb +25 -0
  27. data/lib/textus/application/read/pulse.rb +101 -0
  28. data/lib/textus/application/read/rdeps.rb +35 -0
  29. data/lib/textus/application/read/schema_envelope.rb +26 -0
  30. data/lib/textus/application/read/stale.rb +23 -0
  31. data/lib/textus/application/read/uid.rb +30 -0
  32. data/lib/textus/application/read/validate_all.rb +32 -0
  33. data/lib/textus/application/{reads → read}/validator.rb +2 -2
  34. data/lib/textus/application/read/where.rb +26 -0
  35. data/lib/textus/application/use_case.rb +22 -0
  36. data/lib/textus/application/write/accept.rb +102 -0
  37. data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
  38. data/lib/textus/application/write/delete.rb +45 -0
  39. data/lib/textus/application/{writes → write}/materializer.rb +14 -15
  40. data/lib/textus/application/write/mv.rb +118 -0
  41. data/lib/textus/application/write/publish.rb +96 -0
  42. data/lib/textus/application/write/put.rb +49 -0
  43. data/lib/textus/application/write/refresh_all.rb +63 -0
  44. data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
  45. data/lib/textus/application/write/refresh_worker.rb +134 -0
  46. data/lib/textus/application/write/reject.rb +62 -0
  47. data/lib/textus/{intro.rb → boot.rb} +49 -29
  48. data/lib/textus/builder/pipeline.rb +5 -5
  49. data/lib/textus/cli/group/mcp.rb +9 -0
  50. data/lib/textus/cli/group/zone.rb +9 -0
  51. data/lib/textus/cli/verb/accept.rb +1 -1
  52. data/lib/textus/cli/verb/audit.rb +4 -2
  53. data/lib/textus/cli/verb/blame.rb +1 -1
  54. data/lib/textus/cli/verb/boot.rb +13 -0
  55. data/lib/textus/cli/verb/build.rb +2 -2
  56. data/lib/textus/cli/verb/delete.rb +1 -1
  57. data/lib/textus/cli/verb/deps.rb +1 -1
  58. data/lib/textus/cli/verb/doctor.rb +1 -1
  59. data/lib/textus/cli/verb/freshness.rb +1 -1
  60. data/lib/textus/cli/verb/get.rb +1 -1
  61. data/lib/textus/cli/verb/hook_run.rb +3 -4
  62. data/lib/textus/cli/verb/hooks.rb +11 -14
  63. data/lib/textus/cli/verb/key_delete.rb +24 -0
  64. data/lib/textus/cli/verb/list.rb +1 -1
  65. data/lib/textus/cli/verb/mcp_serve.rb +17 -0
  66. data/lib/textus/cli/verb/migrate.rb +18 -0
  67. data/lib/textus/cli/verb/mv.rb +11 -3
  68. data/lib/textus/cli/verb/published.rb +1 -1
  69. data/lib/textus/cli/verb/pulse.rb +17 -0
  70. data/lib/textus/cli/verb/put.rb +8 -6
  71. data/lib/textus/cli/verb/rdeps.rb +1 -1
  72. data/lib/textus/cli/verb/refresh.rb +1 -1
  73. data/lib/textus/cli/verb/refresh_stale.rb +1 -1
  74. data/lib/textus/cli/verb/reject.rb +1 -1
  75. data/lib/textus/cli/verb/rule_explain.rb +1 -1
  76. data/lib/textus/cli/verb/rule_lint.rb +18 -0
  77. data/lib/textus/cli/verb/schema.rb +1 -1
  78. data/lib/textus/cli/verb/uid.rb +1 -1
  79. data/lib/textus/cli/verb/where.rb +1 -1
  80. data/lib/textus/cli/verb/zone_mv.rb +19 -0
  81. data/lib/textus/cli/verb.rb +4 -4
  82. data/lib/textus/cli.rb +1 -1
  83. data/lib/textus/doctor/check/audit_log.rb +2 -2
  84. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  85. data/lib/textus/doctor/check/hooks.rb +4 -3
  86. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  87. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  88. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  89. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  90. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  91. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  92. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  93. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  94. data/lib/textus/doctor/check/schemas.rb +2 -2
  95. data/lib/textus/doctor/check/sentinels.rb +2 -2
  96. data/lib/textus/doctor/check/templates.rb +2 -2
  97. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  98. data/lib/textus/doctor/check.rb +5 -3
  99. data/lib/textus/doctor.rb +24 -27
  100. data/lib/textus/domain/authorizer.rb +4 -4
  101. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  102. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  103. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  104. data/lib/textus/domain/staleness/generator_check.rb +2 -2
  105. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  106. data/lib/textus/domain/staleness.rb +1 -1
  107. data/lib/textus/errors.rb +16 -0
  108. data/lib/textus/hooks/builtin.rb +14 -14
  109. data/lib/textus/hooks/context.rb +13 -13
  110. data/lib/textus/hooks/error_log.rb +32 -0
  111. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  112. data/lib/textus/hooks/loader.rb +29 -3
  113. data/lib/textus/hooks/rpc_registry.rb +77 -0
  114. data/lib/textus/infra/audit_log.rb +126 -16
  115. data/lib/textus/infra/audit_subscriber.rb +6 -7
  116. data/lib/textus/infra/refresh/detached.rb +1 -1
  117. data/lib/textus/key/path.rb +7 -3
  118. data/lib/textus/manifest/data.rb +78 -0
  119. data/lib/textus/manifest/entry/base.rb +44 -7
  120. data/lib/textus/manifest/entry/derived.rb +41 -6
  121. data/lib/textus/manifest/entry/intake.rb +15 -3
  122. data/lib/textus/manifest/entry/leaf.rb +6 -5
  123. data/lib/textus/manifest/entry/nested.rb +42 -3
  124. data/lib/textus/manifest/entry/parser.rb +8 -44
  125. data/lib/textus/manifest/entry/validators/events.rb +2 -2
  126. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -4
  127. data/lib/textus/manifest/entry/validators/index_filename.rb +2 -1
  128. data/lib/textus/manifest/entry/validators/inject_boot.rb +19 -0
  129. data/lib/textus/manifest/entry/validators/publish_each.rb +4 -3
  130. data/lib/textus/manifest/entry/validators.rb +1 -1
  131. data/lib/textus/manifest/entry.rb +3 -0
  132. data/lib/textus/manifest/policy.rb +48 -0
  133. data/lib/textus/manifest/resolver.rb +18 -18
  134. data/lib/textus/manifest/rules.rb +1 -1
  135. data/lib/textus/manifest/schema.rb +20 -6
  136. data/lib/textus/manifest.rb +53 -101
  137. data/lib/textus/mcp/errors.rb +32 -0
  138. data/lib/textus/mcp/server.rb +127 -0
  139. data/lib/textus/mcp/session.rb +31 -0
  140. data/lib/textus/mcp/tool_schemas.rb +71 -0
  141. data/lib/textus/mcp/tools.rb +129 -0
  142. data/lib/textus/mcp.rb +6 -0
  143. data/lib/textus/schema/tools.rb +14 -10
  144. data/lib/textus/session.rb +84 -0
  145. data/lib/textus/store.rb +17 -8
  146. data/lib/textus/version.rb +1 -1
  147. data/lib/textus.rb +8 -1
  148. metadata +65 -38
  149. data/lib/textus/application/reads/audit.rb +0 -69
  150. data/lib/textus/application/reads/blame.rb +0 -82
  151. data/lib/textus/application/reads/deps.rb +0 -26
  152. data/lib/textus/application/reads/freshness.rb +0 -88
  153. data/lib/textus/application/reads/get.rb +0 -67
  154. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  155. data/lib/textus/application/reads/list.rb +0 -17
  156. data/lib/textus/application/reads/policy_explain.rb +0 -39
  157. data/lib/textus/application/reads/published.rb +0 -17
  158. data/lib/textus/application/reads/rdeps.rb +0 -27
  159. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  160. data/lib/textus/application/reads/stale.rb +0 -15
  161. data/lib/textus/application/reads/uid.rb +0 -23
  162. data/lib/textus/application/reads/validate_all.rb +0 -24
  163. data/lib/textus/application/reads/where.rb +0 -18
  164. data/lib/textus/application/refresh/all.rb +0 -52
  165. data/lib/textus/application/refresh/worker.rb +0 -116
  166. data/lib/textus/application/writes/accept.rb +0 -89
  167. data/lib/textus/application/writes/delete.rb +0 -33
  168. data/lib/textus/application/writes/mv.rb +0 -105
  169. data/lib/textus/application/writes/publish.rb +0 -162
  170. data/lib/textus/application/writes/put.rb +0 -37
  171. data/lib/textus/application/writes/reject.rb +0 -50
  172. data/lib/textus/cli/verb/intro.rb +0 -13
  173. data/lib/textus/infra/event_bus.rb +0 -27
  174. data/lib/textus/manifest/entry/validators/inject_intro.rb +0 -21
  175. data/lib/textus/operations.rb +0 -169
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  check_list = checks&.split(",")&.map(&:strip)
11
- res = Textus::Doctor.run(store, checks: check_list)
11
+ res = Textus::Doctor.run(Textus::Session.for(store), checks: check_list)
12
12
  emit(res, exit_code: res["ok"] ? 0 : 1)
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Textus
8
8
  option :zone, "--zone=Z"
9
9
 
10
10
  def call(store)
11
- rows = operations_for(store).freshness(prefix: prefix, zone: zone)
11
+ rows = session_for(store).freshness(prefix: prefix, zone: zone)
12
12
  emit({ "verb" => "freshness", "rows" => rows })
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("get requires a key")
11
- result = operations_for(store).get_or_refresh(key)
11
+ result = session_for(store).get_or_refresh(key)
12
12
  raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
13
13
 
14
14
  emit(result.to_h_for_wire)
@@ -27,15 +27,14 @@ module Textus
27
27
  end
28
28
 
29
29
  Role.resolve(flag: as_flag, env: ENV, root: store.root)
30
- callable = store.bus.rpc_callable(:resolve_intake, name)
31
30
 
32
31
  begin
33
- Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
34
- callable.call(config: {}, store: store, args: args)
32
+ Timeout.timeout(Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
33
+ store.rpc.invoke(:resolve_intake, name, caps: nil, config: {}, args: args)
35
34
  end
36
35
  rescue Timeout::Error
37
36
  raise UsageError.new(
38
- "hook run '#{name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
37
+ "hook run '#{name}' exceeded #{Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
39
38
  )
40
39
  rescue Textus::Error
41
40
  raise
@@ -16,22 +16,19 @@ module Textus
16
16
  end
17
17
 
18
18
  rows = []
19
- Textus::Hooks::Bus::EVENTS.each do |event, spec|
20
- mode = spec[:mode].to_s
21
- case spec[:mode]
22
- when :rpc
23
- store.bus.rpc_names(event).each do |name|
24
- rows << { "event" => event.to_s, "mode" => mode, "name" => name.to_s }
25
- end
26
- when :pubsub
27
- store.bus.pubsub_handlers(event).each do |h|
28
- row = { "event" => event.to_s, "mode" => mode, "name" => h[:name].to_s }
29
- row["keys"] = Array(h[:keys]) if h[:keys]
30
- rows << row
31
- end
19
+ Textus::Hooks::RpcRegistry::EVENTS.each_key do |event|
20
+ store.rpc.names(event).each do |name|
21
+ rows << { "event" => event.to_s, "mode" => "rpc", "name" => name.to_s }
22
+ end
23
+ end
24
+ Textus::Hooks::EventBus::EVENTS.each_key do |event|
25
+ store.events.pubsub_handlers(event).each do |h|
26
+ row = { "event" => event.to_s, "mode" => "pubsub", "name" => h[:name].to_s }
27
+ row["keys"] = Array(h[:keys]) if h[:keys]
28
+ rows << row
32
29
  end
33
30
  end
34
- store.manifest.entries.each do |e|
31
+ store.manifest.data.entries.each do |e|
35
32
  (e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
36
33
  Array(defs).each do |defn|
37
34
  next unless defn["exec"]
@@ -0,0 +1,24 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class KeyDelete < Verb
5
+ command_name "delete"
6
+ parent_group Group::Key
7
+
8
+ option :as_flag, "--as=ROLE"
9
+ option :dry_run, "--dry-run"
10
+ option :prefix, "--prefix"
11
+
12
+ def call(store)
13
+ if prefix
14
+ p = positional.shift or raise UsageError.new("key delete --prefix requires <prefix>")
15
+ emit(session_for(store).key_delete_prefix(prefix: p, dry_run: dry_run || false).to_h)
16
+ else
17
+ key = positional.shift or raise UsageError.new("key delete requires <key>")
18
+ emit(session_for(store).delete(key))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -8,7 +8,7 @@ module Textus
8
8
  option :zone, "--zone=Z"
9
9
 
10
10
  def call(store)
11
- emit({ "entries" => operations_for(store).list(prefix: prefix, zone: zone) })
11
+ emit({ "entries" => session_for(store).list(prefix: prefix, zone: zone) })
12
12
  end
13
13
  end
14
14
  end
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ # Launches the MCP stdio server in the current process. Blocks on
5
+ # stdin; never returns until stdin closes.
6
+ class MCPServe < Verb
7
+ command_name "serve"
8
+ parent_group Group::MCP
9
+
10
+ def call(store)
11
+ Textus::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout).run
12
+ 0
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Migrate < Verb
5
+ command_name "migrate"
6
+
7
+ option :as_flag, "--as=ROLE"
8
+ option :dry_run, "--dry-run"
9
+
10
+ def call(store)
11
+ path = positional.shift or raise UsageError.new("migrate requires <plan.yaml>")
12
+ plan_yaml = File.read(path)
13
+ emit(session_for(store).migrate(plan_yaml: plan_yaml, dry_run: dry_run || false).to_h)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -7,11 +7,19 @@ module Textus
7
7
 
8
8
  option :as_flag, "--as=ROLE"
9
9
  option :dry_run, "--dry-run"
10
+ option :prefix, "--prefix"
10
11
 
11
12
  def call(store)
12
- old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
13
- new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
14
- emit(operations_for(store).mv(old_key, new_key, dry_run: dry_run || false))
13
+ if prefix
14
+ from_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
15
+ to_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
16
+ emit(session_for(store).key_mv_prefix(from_prefix: from_p, to_prefix: to_p,
17
+ dry_run: dry_run || false).to_h)
18
+ else
19
+ old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
20
+ new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
21
+ emit(session_for(store).mv(old_key, new_key, dry_run: dry_run || false))
22
+ end
15
23
  end
16
24
  end
17
25
  end
@@ -5,7 +5,7 @@ module Textus
5
5
  command_name "published"
6
6
 
7
7
  def call(store)
8
- emit({ "published" => operations_for(store).published })
8
+ emit({ "published" => session_for(store).published })
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class Pulse < Verb
5
+ command_name "pulse"
6
+
7
+ option :since, "--since=N"
8
+
9
+ def call(store)
10
+ ops = session_for(store)
11
+ since_n = (since || "0").to_i
12
+ emit(ops.pulse(since: since_n))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -8,7 +8,7 @@ module Textus
8
8
  option :use_stdin, "--stdin"
9
9
  option :fetch_name, "--fetch=NAME"
10
10
 
11
- def call(store) # rubocop:disable Metrics/AbcSize
11
+ def call(store)
12
12
  key = positional.shift or raise UsageError.new("put requires a key")
13
13
  raise UsageError.new("put requires --stdin in v1") unless use_stdin
14
14
 
@@ -17,15 +17,17 @@ module Textus
17
17
  raw = @stdin.read
18
18
  payload =
19
19
  if fetch_name
20
- callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
21
20
  result =
22
21
  begin
23
- Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
24
- callable.call(config: { "bytes" => raw }, store: store, args: {})
22
+ Timeout.timeout(Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
23
+ store.rpc.invoke(:resolve_intake, fetch_name,
24
+ caps: nil,
25
+ config: { "bytes" => raw },
26
+ args: {})
25
27
  end
26
28
  rescue Timeout::Error
27
29
  raise UsageError.new(
28
- "fetch '#{fetch_name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
30
+ "fetch '#{fetch_name}' exceeded #{Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
29
31
  )
30
32
  end
31
33
  basename = key.split(".").last
@@ -44,7 +46,7 @@ module Textus
44
46
  meta = payload["_meta"] || {}
45
47
  body = payload["body"] || ""
46
48
  if_etag = payload["if_etag"]
47
- result = Textus::Operations.for(store, role: role).put(key, meta: meta, body: body, if_etag: if_etag)
49
+ result = store.session(role: role).put(key, meta: meta, body: body, if_etag: if_etag)
48
50
  emit(result.to_h_for_wire)
49
51
  end
50
52
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("rdeps requires a key")
9
- emit({ "key" => key, "rdeps" => operations_for(store).rdeps(key) })
9
+ emit({ "key" => key, "rdeps" => session_for(store).rdeps(key) })
10
10
  end
11
11
  end
12
12
  end
@@ -6,7 +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
- emit(operations_for(store).refresh(key).to_h_for_wire)
9
+ emit(session_for(store).refresh(key).to_h_for_wire)
10
10
  end
11
11
  end
12
12
  end
@@ -10,7 +10,7 @@ module Textus
10
10
  option :as_flag, "--as=ROLE"
11
11
 
12
12
  def call(store)
13
- result = operations_for(store).refresh_all(prefix: prefix, zone: zone)
13
+ result = session_for(store).refresh_all(prefix: prefix, zone: zone)
14
14
  emit(result)
15
15
  result["ok"] ? 0 : 1
16
16
  end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("reject requires a key")
11
- emit(operations_for(store).reject(key))
11
+ emit(session_for(store).reject(key))
12
12
  end
13
13
  end
14
14
  end
@@ -7,7 +7,7 @@ module Textus
7
7
 
8
8
  def call(store)
9
9
  key = positional.shift or raise UsageError.new("policy explain requires a KEY")
10
- result = operations_for(store).policy_explain(key: key)
10
+ result = session_for(store).policy_explain(key: key)
11
11
  emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
12
12
  end
13
13
  end
@@ -0,0 +1,18 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class RuleLint < Verb
5
+ command_name "lint"
6
+ parent_group Group::Rule
7
+
8
+ option :against, "--against=FILE"
9
+
10
+ def call(store)
11
+ path = against or raise UsageError.new("rule lint --against=FILE required")
12
+ yaml = File.read(path)
13
+ emit(session_for(store).rule_lint(candidate_yaml: yaml).to_h)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -7,7 +7,7 @@ module Textus
7
7
 
8
8
  def call(store)
9
9
  key = positional.shift or raise UsageError.new("schema requires a key")
10
- emit(operations_for(store).schema_envelope(key))
10
+ emit(session_for(store).schema_envelope(key))
11
11
  end
12
12
  end
13
13
  end
@@ -7,7 +7,7 @@ module Textus
7
7
 
8
8
  def call(store)
9
9
  key = positional.shift or raise UsageError.new("uid requires a key")
10
- emit({ "key" => key, "uid" => operations_for(store).uid(key) })
10
+ emit({ "key" => key, "uid" => session_for(store).uid(key) })
11
11
  end
12
12
  end
13
13
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("where requires a key")
9
- emit(operations_for(store).where(key))
9
+ emit(session_for(store).where(key))
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ class CLI
3
+ class Verb
4
+ class ZoneMv < Verb
5
+ command_name "mv"
6
+ parent_group Group::Zone
7
+
8
+ option :as_flag, "--as=ROLE"
9
+ option :dry_run, "--dry-run"
10
+
11
+ def call(store)
12
+ from = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
13
+ to = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
14
+ emit(session_for(store).zone_mv(from: from, to: to, dry_run: dry_run || false).to_h)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -100,12 +100,12 @@ module Textus
100
100
  # Convenience for verbs whose only pre-call boilerplate is
101
101
  # resolving the role and wrapping it in a context.
102
102
  def context_for(store)
103
- Textus::Operations.for(store, role: resolved_role(store)).ctx
103
+ store.session(role: resolved_role(store)).ctx
104
104
  end
105
105
 
106
- # Returns an Operations instance bound to the resolved role.
107
- def operations_for(store)
108
- Textus::Operations.for(store, role: resolved_role(store))
106
+ # Returns a Session instance bound to the resolved role.
107
+ def session_for(store)
108
+ store.session(role: resolved_role(store))
109
109
  end
110
110
  end
111
111
  end
data/lib/textus/cli.rb CHANGED
@@ -99,7 +99,7 @@ module Textus
99
99
  textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]
100
100
  textus blame KEY [--limit=N]
101
101
  textus doctor
102
- textus intro
102
+ textus boot
103
103
 
104
104
  textus key {mv,uid,normalize}
105
105
  textus rule {list,explain}
@@ -3,8 +3,8 @@ module Textus
3
3
  class Check
4
4
  class AuditLog < Check
5
5
  def call
6
- path = File.join(store.root, "audit.log")
7
- Textus::Infra::AuditLog.new(store.root).verify_integrity.map do |v|
6
+ path = File.join(root, "audit.log")
7
+ Textus::Infra::AuditLog.new(root).verify_integrity.map do |v|
8
8
  {
9
9
  "code" => "audit.parse_error",
10
10
  "level" => "warning",
@@ -7,12 +7,12 @@ module Textus
7
7
  class HandlerAllowlist < Check
8
8
  def call
9
9
  out = []
10
- store.manifest.entries.each do |mentry|
10
+ manifest.data.entries.each do |mentry|
11
11
  next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
12
12
 
13
13
  handler = mentry.handler
14
14
 
15
- allow = store.manifest.rules_for(mentry.key).handler_allowlist
15
+ allow = manifest.rules.for(mentry.key).handler_allowlist
16
16
  next if allow.nil?
17
17
  next if allow.allows?(handler)
18
18
 
@@ -4,15 +4,16 @@ module Textus
4
4
  class Hooks < Check
5
5
  def call
6
6
  out = []
7
- dir = File.join(store.root, "hooks")
7
+ dir = File.join(root, "hooks")
8
8
  return out unless File.directory?(dir)
9
9
 
10
10
  Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
11
- bus = Textus::Hooks::Bus.new
11
+ events = Textus::Hooks::EventBus.new
12
+ rpc = Textus::Hooks::RpcRegistry.new
12
13
  Textus.drain_hook_blocks
13
14
  begin
14
15
  load(f)
15
- Textus.drain_hook_blocks.each { |b| b.call(bus) }
16
+ Textus.drain_hook_blocks.each { |b| b.call(Textus::Hooks::Loader::Dsl.new(events: events, rpc: rpc)) }
16
17
  end
17
18
  rescue StandardError, ScriptError => e
18
19
  out << {
@@ -4,10 +4,10 @@ module Textus
4
4
  class IllegalKeys < Check
5
5
  def call
6
6
  out = []
7
- store.manifest.entries.each do |entry|
7
+ manifest.data.entries.each do |entry|
8
8
  next unless entry.nested?
9
9
 
10
- base = File.join(store.root, "zones", entry.path)
10
+ base = File.join(root, "zones", entry.path)
11
11
  next unless File.directory?(base)
12
12
 
13
13
  index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call
8
8
  declared = collect_declared_handlers
9
- registered = store.bus.rpc_names(:resolve_intake).to_set
9
+ registered = rpc.names(:resolve_intake).to_set
10
10
 
11
11
  out = (declared - registered).map do |name|
12
12
  {
@@ -35,7 +35,7 @@ module Textus
35
35
 
36
36
  def collect_declared_handlers
37
37
  set = Set.new
38
- store.manifest.entries.each do |mentry|
38
+ manifest.data.entries.each do |mentry|
39
39
  set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
40
40
  end
41
41
  set
@@ -3,10 +3,10 @@ module Textus
3
3
  class Check
4
4
  class ManifestFiles < Check
5
5
  def call
6
- store.manifest.entries.each_with_object([]) do |entry, out|
6
+ manifest.data.entries.each_with_object([]) do |entry, out|
7
7
  next if entry.nested?
8
8
 
9
- path = Textus::Key::Path.resolve(store.manifest, entry)
9
+ path = Textus::Key::Path.resolve(manifest.data, entry)
10
10
  next if File.exist?(path)
11
11
 
12
12
  out << {
@@ -23,10 +23,10 @@ module Textus
23
23
  }]
24
24
  end
25
25
 
26
- # Doctor check interface: store.root is the .textus/ directory itself,
26
+ # Doctor check interface: root is the .textus/ directory itself,
27
27
  # so manifest.yaml lives directly inside it.
28
28
  def call
29
- path = File.join(store.root, "manifest.yaml")
29
+ path = File.join(root, "manifest.yaml")
30
30
  return [] unless File.exist?(path)
31
31
 
32
32
  doc = YAML.safe_load_file(path, aliases: false) || {}
@@ -1,7 +1,7 @@
1
1
  module Textus
2
2
  module Doctor
3
3
  class Check
4
- # Lists per-key refresh lock files under <store.root>/.locks/ whose
4
+ # Lists per-key refresh lock files under <root>/.locks/ whose
5
5
  # recorded PID is no longer running. These are forensic artifacts only:
6
6
  # Refresh::Lock uses flock(2), which the kernel releases on process
7
7
  # death, so stale files do not block subsequent acquires. The check
@@ -9,7 +9,7 @@ module Textus
9
9
  # (e.g. a refresh path that crashes repeatedly).
10
10
  class RefreshLocks < Check
11
11
  def call
12
- dir = File.join(store.root, ".locks")
12
+ dir = File.join(root, ".locks")
13
13
  return [] unless File.directory?(dir)
14
14
 
15
15
  Dir.glob(File.join(dir, "*.lock")).filter_map { |path| inspect_lock(path) }
@@ -10,8 +10,8 @@ module Textus
10
10
 
11
11
  def call
12
12
  out = []
13
- rules = store.manifest.rules
14
- store.manifest.entries.each do |mentry|
13
+ rules = manifest.rules
14
+ manifest.data.entries.each do |mentry|
15
15
  matches = rules.explain(mentry.key)
16
16
  next if matches.length < 2
17
17
 
@@ -7,7 +7,7 @@ module Textus
7
7
  # leaving the operator with no signal that a schema is broken.
8
8
  class SchemaParseError < Check
9
9
  def call
10
- dir = File.join(store.root, "schemas")
10
+ dir = File.join(root, "schemas")
11
11
  return [] unless File.directory?(dir)
12
12
 
13
13
  Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
@@ -3,7 +3,7 @@ module Textus
3
3
  class Check
4
4
  class SchemaViolations < Check
5
5
  def call
6
- res = Textus::Operations.for(store).validate_all
6
+ res = @session.validate_all
7
7
  res["violations"].map do |v|
8
8
  fix = v["expected"] &&
9
9
  "field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
@@ -4,10 +4,10 @@ module Textus
4
4
  class Schemas < Check
5
5
  def call
6
6
  out = []
7
- store.manifest.entries.each do |entry|
7
+ manifest.data.entries.each do |entry|
8
8
  next if entry.schema.nil?
9
9
 
10
- sp = File.join(store.root, "schemas", "#{entry.schema}.yaml")
10
+ sp = File.join(root, "schemas", "#{entry.schema}.yaml")
11
11
  next if File.exist?(sp)
12
12
 
13
13
  out << {
@@ -3,10 +3,10 @@ module Textus
3
3
  class Check
4
4
  class Sentinels < Check
5
5
  def call
6
- dir = File.join(store.root, "sentinels")
6
+ dir = File.join(root, "sentinels")
7
7
  return [] unless File.directory?(dir)
8
8
 
9
- repo_root = File.dirname(store.root)
9
+ repo_root = File.dirname(root)
10
10
  Dir.glob(File.join(dir, "**", "*#{Textus::Domain::Sentinel::SUFFIX}")).flat_map do |sentinel_path|
11
11
  inspect_sentinel(sentinel_path, repo_root)
12
12
  end
@@ -4,11 +4,11 @@ module Textus
4
4
  class Templates < Check
5
5
  def call
6
6
  out = []
7
- store.manifest.entries.each do |entry|
7
+ manifest.data.entries.each do |entry|
8
8
  template = entry.respond_to?(:template) ? entry.template : nil
9
9
  next if template.nil?
10
10
 
11
- tp = File.join(store.root, "templates", template)
11
+ tp = File.join(root, "templates", template)
12
12
  next if File.exist?(tp)
13
13
 
14
14
  out << {
@@ -3,7 +3,7 @@ module Textus
3
3
  class Check
4
4
  class UnownedSchemaFields < Check
5
5
  def call
6
- dir = File.join(store.root, "schemas")
6
+ dir = File.join(root, "schemas")
7
7
  return [] unless File.directory?(dir)
8
8
 
9
9
  Dir.glob(File.join(dir, "*.yaml")).flat_map do |path|
@@ -14,8 +14,8 @@ module Textus
14
14
  .downcase
15
15
  end
16
16
 
17
- def initialize(store)
18
- @store = store
17
+ def initialize(session)
18
+ @session = session
19
19
  end
20
20
 
21
21
  def call
@@ -24,7 +24,9 @@ module Textus
24
24
 
25
25
  protected
26
26
 
27
- attr_reader :store
27
+ def root = @session.read_caps.root
28
+ def manifest = @session.read_caps.manifest
29
+ def rpc = @session.rpc
28
30
  end
29
31
  end
30
32
  end