textus 0.22.0 → 0.29.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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +195 -48
  3. data/CHANGELOG.md +178 -0
  4. data/README.md +55 -13
  5. data/SPEC.md +79 -42
  6. data/docs/conventions.md +10 -0
  7. data/lib/textus/boot.rb +31 -29
  8. data/lib/textus/builder/pipeline.rb +13 -12
  9. data/lib/textus/call.rb +28 -0
  10. data/lib/textus/cli/group/mcp.rb +9 -0
  11. data/lib/textus/cli/group/zone.rb +9 -0
  12. data/lib/textus/cli/verb/accept.rb +1 -1
  13. data/lib/textus/cli/verb/audit.rb +2 -2
  14. data/lib/textus/cli/verb/blame.rb +1 -1
  15. data/lib/textus/cli/verb/boot.rb +1 -1
  16. data/lib/textus/cli/verb/build.rb +3 -3
  17. data/lib/textus/cli/verb/delete.rb +1 -1
  18. data/lib/textus/cli/verb/deps.rb +1 -1
  19. data/lib/textus/cli/verb/doctor.rb +1 -1
  20. data/lib/textus/cli/verb/freshness.rb +1 -1
  21. data/lib/textus/cli/verb/get.rb +1 -1
  22. data/lib/textus/cli/verb/hook_run.rb +3 -4
  23. data/lib/textus/cli/verb/hooks.rb +11 -14
  24. data/lib/textus/cli/verb/key_delete.rb +24 -0
  25. data/lib/textus/cli/verb/list.rb +1 -1
  26. data/lib/textus/cli/verb/mcp_serve.rb +17 -0
  27. data/lib/textus/cli/verb/migrate.rb +18 -0
  28. data/lib/textus/cli/verb/mv.rb +11 -3
  29. data/lib/textus/cli/verb/published.rb +1 -1
  30. data/lib/textus/cli/verb/pulse.rb +1 -1
  31. data/lib/textus/cli/verb/put.rb +8 -6
  32. data/lib/textus/cli/verb/rdeps.rb +1 -1
  33. data/lib/textus/cli/verb/refresh.rb +1 -1
  34. data/lib/textus/cli/verb/refresh_stale.rb +1 -1
  35. data/lib/textus/cli/verb/reject.rb +1 -1
  36. data/lib/textus/cli/verb/rule_explain.rb +1 -1
  37. data/lib/textus/cli/verb/rule_lint.rb +18 -0
  38. data/lib/textus/cli/verb/schema.rb +1 -1
  39. data/lib/textus/cli/verb/uid.rb +1 -1
  40. data/lib/textus/cli/verb/where.rb +1 -1
  41. data/lib/textus/cli/verb/zone_mv.rb +19 -0
  42. data/lib/textus/cli/verb.rb +7 -7
  43. data/lib/textus/cli.rb +0 -7
  44. data/lib/textus/container.rb +23 -0
  45. data/lib/textus/dispatcher.rb +49 -0
  46. data/lib/textus/doctor/check/audit_log.rb +2 -2
  47. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  48. data/lib/textus/doctor/check/hooks.rb +4 -3
  49. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  50. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  51. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  52. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  53. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  54. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  55. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  56. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  57. data/lib/textus/doctor/check/schemas.rb +2 -2
  58. data/lib/textus/doctor/check/sentinels.rb +11 -9
  59. data/lib/textus/doctor/check/templates.rb +2 -2
  60. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  61. data/lib/textus/doctor/check.rb +12 -3
  62. data/lib/textus/doctor.rb +24 -27
  63. data/lib/textus/domain/authorizer.rb +6 -6
  64. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  65. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  66. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  67. data/lib/textus/domain/sentinel.rb +9 -65
  68. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  69. data/lib/textus/domain/staleness/intake_check.rb +20 -12
  70. data/lib/textus/domain/staleness.rb +4 -4
  71. data/lib/textus/envelope/io/reader.rb +44 -0
  72. data/lib/textus/{application/writes/envelope_io.rb → envelope/io/writer.rb} +32 -58
  73. data/lib/textus/hooks/builtin.rb +14 -14
  74. data/lib/textus/hooks/context.rb +30 -13
  75. data/lib/textus/hooks/error_log.rb +32 -0
  76. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  77. data/lib/textus/hooks/loader.rb +29 -3
  78. data/lib/textus/hooks/rpc_registry.rb +77 -0
  79. data/lib/textus/key/path.rb +7 -3
  80. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  81. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  82. data/lib/textus/maintenance/migrate.rb +51 -0
  83. data/lib/textus/maintenance/rule_lint.rb +56 -0
  84. data/lib/textus/maintenance/zone_mv.rb +51 -0
  85. data/lib/textus/maintenance.rb +15 -0
  86. data/lib/textus/manifest/data.rb +79 -0
  87. data/lib/textus/manifest/entry/base.rb +38 -18
  88. data/lib/textus/manifest/entry/derived.rb +8 -9
  89. data/lib/textus/manifest/entry/nested.rb +7 -9
  90. data/lib/textus/manifest/entry/parser.rb +2 -2
  91. data/lib/textus/manifest/entry/validators/events.rb +2 -2
  92. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  93. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  94. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  95. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  96. data/lib/textus/manifest/entry/validators.rb +2 -2
  97. data/lib/textus/manifest/entry.rb +0 -5
  98. data/lib/textus/manifest/policy.rb +48 -0
  99. data/lib/textus/manifest/resolver.rb +14 -14
  100. data/lib/textus/manifest/rules.rb +1 -1
  101. data/lib/textus/manifest.rb +47 -110
  102. data/lib/textus/mcp/errors.rb +32 -0
  103. data/lib/textus/mcp/server.rb +126 -0
  104. data/lib/textus/mcp/session.rb +40 -0
  105. data/lib/textus/mcp/tool_schemas.rb +71 -0
  106. data/lib/textus/mcp/tools.rb +129 -0
  107. data/lib/textus/mcp.rb +6 -0
  108. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  109. data/lib/textus/{infra → ports}/audit_subscriber.rb +7 -8
  110. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  111. data/lib/textus/{infra → ports}/clock.rb +1 -1
  112. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  113. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  114. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  115. data/lib/textus/ports/sentinel_store.rb +67 -0
  116. data/lib/textus/ports/storage/file_stat.rb +19 -0
  117. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  118. data/lib/textus/projection.rb +91 -0
  119. data/lib/textus/read/audit.rb +111 -0
  120. data/lib/textus/read/blame.rb +81 -0
  121. data/lib/textus/read/boot.rb +18 -0
  122. data/lib/textus/read/deps.rb +24 -0
  123. data/lib/textus/read/doctor.rb +19 -0
  124. data/lib/textus/read/freshness.rb +101 -0
  125. data/lib/textus/read/get.rb +66 -0
  126. data/lib/textus/read/get_or_refresh.rb +69 -0
  127. data/lib/textus/read/list.rb +15 -0
  128. data/lib/textus/read/policy_explain.rb +37 -0
  129. data/lib/textus/read/published.rb +15 -0
  130. data/lib/textus/read/pulse.rb +89 -0
  131. data/lib/textus/read/rdeps.rb +25 -0
  132. data/lib/textus/read/schema_envelope.rb +16 -0
  133. data/lib/textus/read/stale.rb +17 -0
  134. data/lib/textus/read/uid.rb +20 -0
  135. data/lib/textus/read/validate_all.rb +22 -0
  136. data/lib/textus/read/validator.rb +84 -0
  137. data/lib/textus/read/where.rb +16 -0
  138. data/lib/textus/role_scope.rb +49 -0
  139. data/lib/textus/schema/tools.rb +14 -10
  140. data/lib/textus/store.rb +25 -11
  141. data/lib/textus/version.rb +1 -1
  142. data/lib/textus/write/accept.rb +86 -0
  143. data/lib/textus/write/authority_gate.rb +24 -0
  144. data/lib/textus/write/delete.rb +54 -0
  145. data/lib/textus/write/materializer.rb +48 -0
  146. data/lib/textus/write/mv.rb +123 -0
  147. data/lib/textus/write/publish.rb +66 -0
  148. data/lib/textus/write/put.rb +59 -0
  149. data/lib/textus/write/refresh_all.rb +44 -0
  150. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  151. data/lib/textus/write/refresh_worker.rb +138 -0
  152. data/lib/textus/write/reject.rb +54 -0
  153. data/lib/textus.rb +7 -1
  154. metadata +75 -46
  155. data/lib/textus/application/context.rb +0 -34
  156. data/lib/textus/application/projection.rb +0 -91
  157. data/lib/textus/application/reads/audit.rb +0 -94
  158. data/lib/textus/application/reads/blame.rb +0 -82
  159. data/lib/textus/application/reads/deps.rb +0 -26
  160. data/lib/textus/application/reads/freshness.rb +0 -88
  161. data/lib/textus/application/reads/get.rb +0 -67
  162. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  163. data/lib/textus/application/reads/list.rb +0 -17
  164. data/lib/textus/application/reads/policy_explain.rb +0 -39
  165. data/lib/textus/application/reads/published.rb +0 -17
  166. data/lib/textus/application/reads/pulse.rb +0 -63
  167. data/lib/textus/application/reads/rdeps.rb +0 -27
  168. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  169. data/lib/textus/application/reads/stale.rb +0 -15
  170. data/lib/textus/application/reads/uid.rb +0 -23
  171. data/lib/textus/application/reads/validate_all.rb +0 -24
  172. data/lib/textus/application/reads/validator.rb +0 -86
  173. data/lib/textus/application/reads/where.rb +0 -18
  174. data/lib/textus/application/refresh/all.rb +0 -52
  175. data/lib/textus/application/refresh/orchestrator.rb +0 -78
  176. data/lib/textus/application/refresh/worker.rb +0 -116
  177. data/lib/textus/application/writes/accept.rb +0 -89
  178. data/lib/textus/application/writes/authority_gate.rb +0 -26
  179. data/lib/textus/application/writes/delete.rb +0 -33
  180. data/lib/textus/application/writes/materializer.rb +0 -50
  181. data/lib/textus/application/writes/mv.rb +0 -105
  182. data/lib/textus/application/writes/publish.rb +0 -81
  183. data/lib/textus/application/writes/put.rb +0 -37
  184. data/lib/textus/application/writes/reject.rb +0 -50
  185. data/lib/textus/infra/event_bus.rb +0 -27
  186. data/lib/textus/operations.rb +0 -176
@@ -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
@@ -7,7 +7,7 @@ module Textus
7
7
  option :since, "--since=N"
8
8
 
9
9
  def call(store)
10
- ops = operations_for(store)
10
+ ops = session_for(store)
11
11
  since_n = (since || "0").to_i
12
12
  emit(ops.pulse(since: since_n))
13
13
  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::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::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.as(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
@@ -96,16 +96,16 @@ module Textus
96
96
  Role.resolve(flag: flag, env: ENV, root: store.root)
97
97
  end
98
98
 
99
- # Returns an Application::Context bound to the resolved role.
100
- # Convenience for verbs whose only pre-call boilerplate is
101
- # resolving the role and wrapping it in a context.
99
+ # Returns a Call value bound to the resolved role. Convenience for
100
+ # verbs whose only pre-call boilerplate is resolving the role and
101
+ # wrapping it in a Call.
102
102
  def context_for(store)
103
- Textus::Operations.for(store, role: resolved_role(store)).ctx
103
+ Textus::Call.build(role: resolved_role(store))
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 RoleScope bound to the resolved role.
107
+ def session_for(store)
108
+ store.as(resolved_role(store))
109
109
  end
110
110
  end
111
111
  end
data/lib/textus/cli.rb CHANGED
@@ -14,13 +14,6 @@ module Textus
14
14
  .to_h { |k| [k.command_name, k] }
15
15
  end
16
16
 
17
- # Backward-compat constant; callers should prefer `CLI.verbs`.
18
- def self.const_missing(name)
19
- return verbs.freeze if name == :VERBS
20
-
21
- super
22
- end
23
-
24
17
  def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
25
18
  new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
26
19
  end
@@ -0,0 +1,23 @@
1
+ module Textus
2
+ # Single capability record handed to every use case. Replaces the
3
+ # ReadCaps/WriteCaps/HookCaps trio from 0.26.x. Built once per Store.
4
+ Container = Data.define(
5
+ :manifest, :file_store, :schemas, :root,
6
+ :audit_log, :events, :rpc, :authorizer
7
+ )
8
+
9
+ class Container
10
+ def self.from_store(store)
11
+ new(
12
+ manifest: store.manifest,
13
+ file_store: store.file_store,
14
+ schemas: store.schemas,
15
+ root: store.root,
16
+ audit_log: store.audit_log,
17
+ events: store.events,
18
+ rpc: store.rpc,
19
+ authorizer: Textus::Domain::Authorizer.new(manifest: store.manifest),
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Textus
2
+ # Static verb → use-case map. Canonical lookup as of 0.27.0; replaces the
3
+ # Application::UseCase registry whose entries were populated by file-load
4
+ # side effects in 0.26.x.
5
+ module Dispatcher
6
+ VERBS = {
7
+ # Write
8
+ put: Textus::Write::Put,
9
+ delete: Textus::Write::Delete,
10
+ mv: Textus::Write::Mv,
11
+ accept: Textus::Write::Accept,
12
+ reject: Textus::Write::Reject,
13
+ publish: Textus::Write::Publish,
14
+ refresh: Textus::Write::RefreshWorker,
15
+ refresh_all: Textus::Write::RefreshAll,
16
+
17
+ # Read
18
+ get: Textus::Read::Get,
19
+ get_or_refresh: Textus::Read::GetOrRefresh,
20
+ list: Textus::Read::List,
21
+ where: Textus::Read::Where,
22
+ uid: Textus::Read::Uid,
23
+ blame: Textus::Read::Blame,
24
+ audit: Textus::Read::Audit,
25
+ freshness: Textus::Read::Freshness,
26
+ stale: Textus::Read::Stale,
27
+ deps: Textus::Read::Deps,
28
+ rdeps: Textus::Read::Rdeps,
29
+ pulse: Textus::Read::Pulse,
30
+ policy_explain: Textus::Read::PolicyExplain,
31
+ published: Textus::Read::Published,
32
+ schema_envelope: Textus::Read::SchemaEnvelope,
33
+ validate_all: Textus::Read::ValidateAll,
34
+ doctor: Textus::Read::Doctor,
35
+ boot: Textus::Read::Boot,
36
+
37
+ # Maintenance
38
+ migrate: Textus::Maintenance::Migrate,
39
+ zone_mv: Textus::Maintenance::ZoneMv,
40
+ key_mv_prefix: Textus::Maintenance::KeyMvPrefix,
41
+ key_delete_prefix: Textus::Maintenance::KeyDeletePrefix,
42
+ rule_lint: Textus::Maintenance::RuleLint,
43
+ }.freeze
44
+
45
+ def self.fetch(verb)
46
+ VERBS.fetch(verb.to_sym) { raise UsageError.new("unknown verb: #{verb.inspect}") }
47
+ end
48
+ end
49
+ end
@@ -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::Ports::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 = dispatch(: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,22 +3,24 @@ module Textus
3
3
  class Check
4
4
  class Sentinels < Check
5
5
  def call
6
- dir = File.join(store.root, "sentinels")
7
- return [] unless File.directory?(dir)
6
+ store = Textus::Ports::SentinelStore.new
7
+ file_stat = Textus::Ports::Storage::FileStat.new
8
+ dir = File.join(root, "sentinels")
9
+ return [] unless file_stat.directory?(dir)
8
10
 
9
- repo_root = File.dirname(store.root)
10
- Dir.glob(File.join(dir, "**", "*#{Textus::Domain::Sentinel::SUFFIX}")).flat_map do |sentinel_path|
11
- inspect_sentinel(sentinel_path, repo_root)
11
+ repo_root = File.dirname(root)
12
+ file_stat.glob(File.join(dir, "**", "*#{Textus::Ports::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
13
+ inspect_sentinel(sentinel_path, repo_root, store, file_stat)
12
14
  end
13
15
  end
14
16
 
15
17
  private
16
18
 
17
- def inspect_sentinel(sentinel_path, repo_root)
18
- sentinel = Textus::Domain::Sentinel.load(sentinel_path, repo_root)
19
+ def inspect_sentinel(sentinel_path, repo_root, store, file_stat)
20
+ sentinel = store.load(sentinel_path, repo_root)
19
21
  return [parse_error_issue(sentinel_path)] if sentinel.nil?
20
- return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?
21
- return [drift_issue(sentinel)] if sentinel.drift?
22
+ return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?(file_stat)
23
+ return [drift_issue(sentinel)] if sentinel.drift?(file_stat)
22
24
 
23
25
  []
24
26
  end