textus 0.22.0 → 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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +148 -45
  3. data/CHANGELOG.md +102 -0
  4. data/README.md +1 -1
  5. data/SPEC.md +12 -12
  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/boot.rb +27 -29
  48. data/lib/textus/builder/pipeline.rb +3 -3
  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 +2 -2
  53. data/lib/textus/cli/verb/blame.rb +1 -1
  54. data/lib/textus/cli/verb/boot.rb +1 -1
  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 +1 -1
  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/doctor/check/audit_log.rb +2 -2
  83. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  84. data/lib/textus/doctor/check/hooks.rb +4 -3
  85. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  86. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  87. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  88. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  89. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  90. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  91. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  92. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  93. data/lib/textus/doctor/check/schemas.rb +2 -2
  94. data/lib/textus/doctor/check/sentinels.rb +2 -2
  95. data/lib/textus/doctor/check/templates.rb +2 -2
  96. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  97. data/lib/textus/doctor/check.rb +5 -3
  98. data/lib/textus/doctor.rb +24 -27
  99. data/lib/textus/domain/authorizer.rb +4 -4
  100. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  101. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  102. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  103. data/lib/textus/domain/staleness/generator_check.rb +2 -2
  104. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  105. data/lib/textus/domain/staleness.rb +1 -1
  106. data/lib/textus/hooks/builtin.rb +14 -14
  107. data/lib/textus/hooks/context.rb +13 -13
  108. data/lib/textus/hooks/error_log.rb +32 -0
  109. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  110. data/lib/textus/hooks/loader.rb +29 -3
  111. data/lib/textus/hooks/rpc_registry.rb +77 -0
  112. data/lib/textus/infra/audit_subscriber.rb +6 -7
  113. data/lib/textus/infra/refresh/detached.rb +1 -1
  114. data/lib/textus/key/path.rb +7 -3
  115. data/lib/textus/manifest/data.rb +78 -0
  116. data/lib/textus/manifest/entry/base.rb +4 -4
  117. data/lib/textus/manifest/entry/derived.rb +4 -5
  118. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  119. data/lib/textus/manifest/policy.rb +48 -0
  120. data/lib/textus/manifest/resolver.rb +14 -14
  121. data/lib/textus/manifest/rules.rb +1 -1
  122. data/lib/textus/manifest.rb +53 -111
  123. data/lib/textus/mcp/errors.rb +32 -0
  124. data/lib/textus/mcp/server.rb +127 -0
  125. data/lib/textus/mcp/session.rb +31 -0
  126. data/lib/textus/mcp/tool_schemas.rb +71 -0
  127. data/lib/textus/mcp/tools.rb +129 -0
  128. data/lib/textus/mcp.rb +6 -0
  129. data/lib/textus/schema/tools.rb +14 -10
  130. data/lib/textus/session.rb +84 -0
  131. data/lib/textus/store.rb +14 -9
  132. data/lib/textus/version.rb +1 -1
  133. data/lib/textus.rb +8 -1
  134. metadata +61 -36
  135. data/lib/textus/application/reads/audit.rb +0 -94
  136. data/lib/textus/application/reads/blame.rb +0 -82
  137. data/lib/textus/application/reads/deps.rb +0 -26
  138. data/lib/textus/application/reads/freshness.rb +0 -88
  139. data/lib/textus/application/reads/get.rb +0 -67
  140. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  141. data/lib/textus/application/reads/list.rb +0 -17
  142. data/lib/textus/application/reads/policy_explain.rb +0 -39
  143. data/lib/textus/application/reads/published.rb +0 -17
  144. data/lib/textus/application/reads/pulse.rb +0 -63
  145. data/lib/textus/application/reads/rdeps.rb +0 -27
  146. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  147. data/lib/textus/application/reads/stale.rb +0 -15
  148. data/lib/textus/application/reads/uid.rb +0 -23
  149. data/lib/textus/application/reads/validate_all.rb +0 -24
  150. data/lib/textus/application/reads/where.rb +0 -18
  151. data/lib/textus/application/refresh/all.rb +0 -52
  152. data/lib/textus/application/refresh/worker.rb +0 -116
  153. data/lib/textus/application/writes/accept.rb +0 -89
  154. data/lib/textus/application/writes/delete.rb +0 -33
  155. data/lib/textus/application/writes/mv.rb +0 -105
  156. data/lib/textus/application/writes/publish.rb +0 -81
  157. data/lib/textus/application/writes/put.rb +0 -37
  158. data/lib/textus/application/writes/reject.rb +0 -50
  159. data/lib/textus/infra/event_bus.rb +0 -27
  160. data/lib/textus/operations.rb +0 -176
@@ -0,0 +1,134 @@
1
+ require "timeout"
2
+
3
+ module Textus
4
+ module Application
5
+ module Write
6
+ module RefreshWorker
7
+ FETCH_TIMEOUT_SECONDS = 30
8
+
9
+ def self.call(*, session:, ctx:, caps:, **)
10
+ Impl.new(
11
+ ctx: ctx, caps: caps,
12
+ rpc: session.rpc,
13
+ writer: session.envelope_writer,
14
+ hook_context: session.hook_context
15
+ ).call(*, **)
16
+ end
17
+
18
+ class Impl
19
+ def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
20
+ @ctx = ctx
21
+ @caps = caps
22
+ @manifest = caps.manifest
23
+ @writer = writer
24
+ @events = caps.events
25
+ @rpc = rpc
26
+ @authorizer = caps.authorizer
27
+ @hook_context = hook_context
28
+ end
29
+
30
+ # call(key) is the primary entry; run is kept as an alias for
31
+ # Orchestrator and RefreshAll which call worker.run(key).
32
+ def call(key)
33
+ run(key)
34
+ end
35
+
36
+ def run(key)
37
+ res = @manifest.resolver.resolve(key)
38
+ mentry = res.entry
39
+ path = res.path
40
+ remaining = res.remaining
41
+ raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
42
+
43
+ before_etag = File.exist?(path) ? Etag.for_file(path) : nil
44
+ result = fetch_with_events(key, mentry, remaining)
45
+ persist_and_notify(key, mentry, result, before_etag)
46
+ end
47
+
48
+ private
49
+
50
+ def fetch_timeout_for(key)
51
+ rule = @manifest.rules.for(key)
52
+ rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
53
+ end
54
+
55
+ def fetch_with_events(key, mentry, remaining)
56
+ @events.publish(:refresh_started, ctx: @hook_context, key: key, mode: :sync)
57
+ call_intake(key, mentry, remaining)
58
+ end
59
+
60
+ def call_intake(key, mentry, remaining)
61
+ timeout = fetch_timeout_for(key)
62
+ Timeout.timeout(timeout) do
63
+ @rpc.invoke(:resolve_intake, mentry.handler,
64
+ caps: @caps,
65
+ config: mentry.config,
66
+ args: { trigger_key: key, leaf_segments: remaining || [] })
67
+ end
68
+ rescue Timeout::Error
69
+ @events.publish(:refresh_failed, ctx: @hook_context, key: key,
70
+ error_class: "Timeout::Error",
71
+ error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
72
+ raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
73
+ rescue Textus::Error => e
74
+ @events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
75
+ error_message: e.message)
76
+ raise
77
+ rescue StandardError => e
78
+ @events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
79
+ error_message: e.message)
80
+ raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
81
+ end
82
+
83
+ def persist_and_notify(key, mentry, result, before_etag)
84
+ normalized = RefreshWorker.send(:normalize_action_result, result, format: mentry.format)
85
+ @authorizer.authorize_write!(mentry, role: @ctx.role)
86
+ envelope = @writer.put(
87
+ key,
88
+ mentry: mentry,
89
+ payload: Textus::Application::Envelope::Writer::Payload.new(
90
+ meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
91
+ ),
92
+ )
93
+ change = detect_change(before_etag, envelope)
94
+ @events.publish(:entry_refreshed, ctx: @hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
95
+ envelope
96
+ end
97
+
98
+ def detect_change(before_etag, envelope)
99
+ if before_etag.nil? then :created
100
+ elsif envelope.etag == before_etag then :unchanged
101
+ else :updated
102
+ end
103
+ end
104
+ end
105
+
106
+ def self.normalize_action_result(res, format:)
107
+ res = res.transform_keys(&:to_s) if res.is_a?(Hash)
108
+ res ||= {}
109
+ meta_val = res["_meta"]
110
+ body = res["body"]
111
+ content = res["content"]
112
+
113
+ case format
114
+ when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
115
+ when "text" then { meta: {}, body: body.to_s, content: nil }
116
+ when "json", "yaml"
117
+ if !content.nil?
118
+ { meta: meta_val || {}, body: nil, content: content }
119
+ elsif !body.nil?
120
+ { meta: {}, body: body.to_s, content: nil }
121
+ else
122
+ raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
123
+ end
124
+ else
125
+ raise Textus::UsageError.new("unknown format #{format.inspect}")
126
+ end
127
+ end
128
+ private_class_method :normalize_action_result
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ Textus::Application::UseCase.register(:refresh, Textus::Application::Write::RefreshWorker, caps: :write)
@@ -0,0 +1,62 @@
1
+ require_relative "authority_gate"
2
+
3
+ module Textus
4
+ module Application
5
+ module Write
6
+ module Reject
7
+ def self.call(*, session:, ctx:, caps:, **)
8
+ Impl.new(
9
+ ctx: ctx, caps: caps,
10
+ writer: session.envelope_writer,
11
+ hook_context: session.hook_context
12
+ ).call(*, **)
13
+ end
14
+
15
+ class Impl
16
+ include AuthorityGate
17
+
18
+ def initialize(ctx:, caps:, writer:, hook_context:)
19
+ @ctx = ctx
20
+ @caps = caps
21
+ @manifest = caps.manifest
22
+ @writer = writer
23
+ @events = caps.events
24
+ @authorizer = caps.authorizer
25
+ @hook_context = hook_context
26
+ end
27
+
28
+ def call(pending_key)
29
+ assert_accept_authority!("reject")
30
+
31
+ mentry = @manifest.resolver.resolve(pending_key).entry
32
+ unless mentry.in_proposal_zone?
33
+ raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
34
+ end
35
+
36
+ env = Textus::Application::Read::Get::Impl.new(
37
+ ctx: @ctx, caps: @caps,
38
+ ).call(pending_key)
39
+ proposal = env.meta&.dig("proposal") or
40
+ raise ProposalError.new("entry has no proposal block: #{pending_key}")
41
+ target_key = proposal["target_key"] or
42
+ raise ProposalError.new("proposal missing target_key")
43
+
44
+ Textus::Application::Write::Delete::Impl.new(
45
+ ctx: @ctx, caps: @caps, writer: @writer,
46
+ hook_context: @hook_context
47
+ ).call(pending_key, suppress_events: true)
48
+
49
+ @events.publish(:proposal_rejected,
50
+ ctx: @hook_context,
51
+ key: pending_key,
52
+ target_key: target_key)
53
+
54
+ { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ Textus::Application::UseCase.register(:reject, Textus::Application::Write::Reject, caps: :write)
data/lib/textus/boot.rb CHANGED
@@ -26,7 +26,7 @@ module Textus
26
26
  "edit files in identity/working zones, then 'textus put KEY --as=#{name}'"
27
27
  end,
28
28
  proposer: lambda do |name, manifest|
29
- authority = manifest.roles_with_kind(:accept_authority).first || "accept_authority"
29
+ authority = manifest.policy.roles_with_kind(:accept_authority).first || "accept_authority"
30
30
  "propose changes by writing review.* entries with --as=#{name} and a 'proposal:' frontmatter block; " \
31
31
  "the #{authority} role runs 'textus accept' to apply"
32
32
  end,
@@ -39,7 +39,7 @@ module Textus
39
39
  }.freeze
40
40
 
41
41
  def self.write_flows_for(manifest)
42
- manifest.role_mapping.each_with_object({}) do |(name, kind), acc|
42
+ manifest.policy.role_mapping.each_with_object({}) do |(name, kind), acc|
43
43
  tmpl = WRITE_FLOW_TEMPLATES[kind]
44
44
  acc[name] = tmpl.call(name, manifest) if tmpl
45
45
  end
@@ -120,11 +120,11 @@ module Textus
120
120
  "summary" => "delta since cursor — changed entries, stale, pending review, doctor summary" },
121
121
  ].freeze
122
122
 
123
- def self.agent_quickstart(manifest, store)
124
- proposer_roles = manifest.roles_with_kind(:proposer)
123
+ def self.agent_quickstart(manifest, session)
124
+ proposer_roles = manifest.policy.roles_with_kind(:proposer)
125
125
  agent_role = proposer_roles.first
126
126
 
127
- writable_zones = manifest.zones.each_with_object([]) do |(zname, writers), acc|
127
+ writable_zones = manifest.data.zones.each_with_object([]) do |(zname, writers), acc|
128
128
  acc << zname if agent_role && writers.include?(agent_role)
129
129
  end
130
130
 
@@ -135,7 +135,7 @@ module Textus
135
135
  "write_verbs" => agent_role ? ["put KEY --as=#{agent_role} --stdin"] : [],
136
136
  "writable_zones" => writable_zones,
137
137
  "propose_zone" => propose_zone,
138
- "latest_seq" => store.audit_log.latest_seq,
138
+ "latest_seq" => session.write_caps.audit_log.latest_seq,
139
139
  }
140
140
  end
141
141
 
@@ -144,29 +144,30 @@ module Textus
144
144
  "role_resolution" => {
145
145
  "summary" => "write role is resolved in order: --as flag, TEXTUS_ROLE env var, .textus/role file, " \
146
146
  "default 'human'",
147
- "roles" => manifest.role_mapping.keys,
147
+ "roles" => manifest.policy.role_mapping.keys,
148
148
  "ref" => "SPEC.md §5",
149
149
  },
150
150
  )
151
151
  end
152
152
 
153
- def self.run(store)
153
+ def self.run(session)
154
+ manifest = session.read_caps.manifest
154
155
  {
155
156
  "protocol" => PROTOCOL_ID,
156
- "store_root" => store.root,
157
- "zones" => zones_for(store),
158
- "entries" => entries_for(store),
159
- "hooks" => hooks_for(store),
160
- "write_flows" => write_flows_for(store.manifest),
157
+ "store_root" => session.read_caps.root,
158
+ "zones" => zones_for(manifest),
159
+ "entries" => entries_for(manifest),
160
+ "hooks" => hooks_for(session),
161
+ "write_flows" => write_flows_for(manifest),
161
162
  "cli_verbs" => CLI_VERBS.map(&:dup),
162
- "agent_protocol" => agent_protocol(store.manifest),
163
- "agent_quickstart" => agent_quickstart(store.manifest, store),
163
+ "agent_protocol" => agent_protocol(manifest),
164
+ "agent_quickstart" => agent_quickstart(manifest, session),
164
165
  "docs" => { "spec" => "SPEC.md", "example" => "examples/claude-plugin/" },
165
166
  }
166
167
  end
167
168
 
168
- def self.zones_for(store)
169
- store.manifest.zones.map do |name, writers|
169
+ def self.zones_for(manifest)
170
+ manifest.data.zones.map do |name, writers|
170
171
  row = { "name" => name, "writers" => Array(writers) }
171
172
  purpose = ZONE_PURPOSES[name]
172
173
  row["purpose"] = purpose if purpose
@@ -174,9 +175,9 @@ module Textus
174
175
  end
175
176
  end
176
177
 
177
- def self.entries_for(store)
178
- store.manifest.entries.map do |e|
179
- derived = store.manifest.zone_kinds(e.zone).include?(:generator)
178
+ def self.entries_for(manifest)
179
+ manifest.data.entries.map do |e|
180
+ derived = manifest.policy.zone_kinds(e.zone).include?(:generator)
180
181
  {
181
182
  "key" => e.key,
182
183
  "zone" => e.zone,
@@ -192,16 +193,13 @@ module Textus
192
193
  end
193
194
  end
194
195
 
195
- def self.hooks_for(store)
196
- bus = store.bus
196
+ def self.hooks_for(session)
197
197
  sections = {}
198
- Hooks::Bus::EVENTS.each do |event, spec|
199
- case spec[:mode]
200
- when :rpc
201
- sections[event.to_s] = bus.rpc_names(event).map(&:to_s).sort
202
- when :pubsub
203
- sections[event.to_s] = bus.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
204
- end
198
+ Hooks::RpcRegistry::EVENTS.each_key do |event|
199
+ sections[event.to_s] = session.rpc.names(event).map(&:to_s).sort
200
+ end
201
+ Hooks::EventBus::EVENTS.each_key do |event|
202
+ sections[event.to_s] = session.events.pubsub_handlers(event).map { |h| h[:name].to_s }.sort
205
203
  end
206
204
  sections
207
205
  end
@@ -63,7 +63,7 @@ module Textus
63
63
  end
64
64
 
65
65
  # rubocop:disable Metrics/ParameterLists
66
- def self.run(mentry:, manifest:, reader:, lister:, transform_resolver:, template_loader:,
66
+ def self.run(mentry:, manifest:, reader:, lister:, rpc:, template_loader:,
67
67
  transform_context: nil, inject_boot: nil)
68
68
  # 1. Load sources + project + reduce
69
69
  data =
@@ -72,7 +72,7 @@ module Textus
72
72
  reader: reader,
73
73
  spec: mentry.source.to_h.transform_keys(&:to_s),
74
74
  lister: lister,
75
- transform_resolver: transform_resolver,
75
+ rpc: rpc,
76
76
  transform_context: transform_context,
77
77
  ).run
78
78
  else
@@ -86,7 +86,7 @@ module Textus
86
86
  bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
87
87
 
88
88
  # 3. Write (idempotent: skip if only generated_at would differ)
89
- target_path = Key::Path.resolve(manifest, mentry)
89
+ target_path = Key::Path.resolve(manifest.data, mentry)
90
90
  FileUtils.mkdir_p(File.dirname(target_path))
91
91
  write_if_changed(target_path, bytes, mentry.format)
92
92
 
@@ -0,0 +1,9 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class MCP < Group
5
+ command_name "mcp"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Textus
2
+ class CLI
3
+ class Group
4
+ class Zone < Group
5
+ command_name "zone"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("accept requires a key")
11
- emit(operations_for(store).accept(key))
11
+ emit(session_for(store).accept(key))
12
12
  end
13
13
  end
14
14
  end
@@ -14,8 +14,8 @@ module Textus
14
14
  option :limit, "--limit=N"
15
15
 
16
16
  def call(store)
17
- ops = operations_for(store)
18
- since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ops.ctx.now)
17
+ ops = session_for(store)
18
+ since_time = since && Textus::Application::Read::Audit.parse_since(since, now: ops.ctx.now)
19
19
  rows = ops.audit(
20
20
  key: key_filter,
21
21
  zone: zone,
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("blame requires a key")
11
- rows = operations_for(store).blame(key: key, limit: limit&.to_i)
11
+ rows = session_for(store).blame(key: key, limit: limit&.to_i)
12
12
  emit({ "verb" => "blame", "key" => key, "rows" => rows })
13
13
  end
14
14
  end
@@ -5,7 +5,7 @@ module Textus
5
5
  command_name "boot"
6
6
 
7
7
  def call(store)
8
- emit(Textus::Boot.run(store))
8
+ emit(Textus::Boot.run(Textus::Session.for(store)))
9
9
  end
10
10
  end
11
11
  end
@@ -8,8 +8,8 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  Textus::Infra::BuildLock.with(root: store.root) do
11
- role = store.manifest.roles_with_kind(:generator).first || "builder"
12
- ops = Textus::Operations.for(store, role: role)
11
+ role = store.manifest.policy.roles_with_kind(:generator).first || "builder"
12
+ ops = store.session(role: role)
13
13
  result = ops.publish(prefix: prefix)
14
14
  emit(result)
15
15
  end
@@ -9,7 +9,7 @@ module Textus
9
9
 
10
10
  def call(store)
11
11
  key = positional.shift or raise UsageError.new("delete requires a key")
12
- emit(operations_for(store).delete(key, if_etag: if_etag))
12
+ emit(session_for(store).delete(key, if_etag: if_etag))
13
13
  end
14
14
  end
15
15
  end
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call(store)
8
8
  key = positional.shift or raise UsageError.new("deps requires a key")
9
- emit({ "key" => key, "deps" => operations_for(store).deps(key) })
9
+ emit({ "key" => key, "deps" => session_for(store).deps(key) })
10
10
  end
11
11
  end
12
12
  end
@@ -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
@@ -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