textus 0.26.0 → 0.30.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +118 -68
  3. data/CHANGELOG.md +132 -0
  4. data/README.md +61 -19
  5. data/SPEC.md +107 -46
  6. data/docs/conventions.md +4 -4
  7. data/lib/textus/boot.rb +18 -12
  8. data/lib/textus/builder/pipeline.rb +13 -12
  9. data/lib/textus/call.rb +28 -0
  10. data/lib/textus/cli/verb/audit.rb +1 -1
  11. data/lib/textus/cli/verb/boot.rb +1 -1
  12. data/lib/textus/cli/verb/build.rb +2 -2
  13. data/lib/textus/cli/verb/doctor.rb +1 -1
  14. data/lib/textus/cli/verb/hook_run.rb +2 -6
  15. data/lib/textus/cli/verb/put.rb +5 -14
  16. data/lib/textus/cli/verb/retain.rb +19 -0
  17. data/lib/textus/cli/verb/rule_list.rb +1 -1
  18. data/lib/textus/cli/verb.rb +6 -6
  19. data/lib/textus/cli.rb +19 -23
  20. data/lib/textus/container.rb +23 -0
  21. data/lib/textus/dispatcher.rb +57 -0
  22. data/lib/textus/doctor/check/audit_log.rb +1 -1
  23. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  24. data/lib/textus/doctor/check/sentinels.rb +10 -8
  25. data/lib/textus/doctor/check.rb +15 -5
  26. data/lib/textus/doctor.rb +7 -7
  27. data/lib/textus/domain/authorizer.rb +2 -2
  28. data/lib/textus/domain/duration.rb +22 -0
  29. data/lib/textus/domain/policy/refresh.rb +1 -15
  30. data/lib/textus/domain/policy/retention.rb +26 -0
  31. data/lib/textus/domain/retention.rb +44 -0
  32. data/lib/textus/domain/sentinel.rb +9 -65
  33. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  34. data/lib/textus/domain/staleness/intake_check.rb +18 -10
  35. data/lib/textus/domain/staleness.rb +3 -3
  36. data/lib/textus/{application/envelope → envelope/io}/reader.rb +6 -2
  37. data/lib/textus/{application/envelope → envelope/io}/writer.rb +19 -11
  38. data/lib/textus/hooks/context.rb +30 -13
  39. data/lib/textus/hooks/event_bus.rb +8 -20
  40. data/lib/textus/hooks/rpc_registry.rb +9 -35
  41. data/lib/textus/hooks/signature.rb +31 -0
  42. data/lib/textus/init.rb +7 -6
  43. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  44. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  45. data/lib/textus/maintenance/migrate.rb +51 -0
  46. data/lib/textus/maintenance/rule_lint.rb +56 -0
  47. data/lib/textus/maintenance/zone_mv.rb +51 -0
  48. data/lib/textus/maintenance.rb +15 -0
  49. data/lib/textus/manifest/data.rb +9 -4
  50. data/lib/textus/manifest/entry/base.rb +38 -18
  51. data/lib/textus/manifest/entry/derived.rb +6 -6
  52. data/lib/textus/manifest/entry/nested.rb +7 -9
  53. data/lib/textus/manifest/entry/parser.rb +2 -2
  54. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  55. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  56. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  57. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  58. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  59. data/lib/textus/manifest/entry/validators.rb +2 -2
  60. data/lib/textus/manifest/entry.rb +0 -5
  61. data/lib/textus/manifest/policy.rb +34 -7
  62. data/lib/textus/manifest/rules.rb +10 -1
  63. data/lib/textus/manifest/schema.rb +54 -4
  64. data/lib/textus/manifest.rb +4 -8
  65. data/lib/textus/mcp/server.rb +2 -11
  66. data/lib/textus/mcp/session.rb +13 -20
  67. data/lib/textus/mcp/tools.rb +2 -2
  68. data/lib/textus/mcp.rb +1 -1
  69. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  70. data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
  71. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  72. data/lib/textus/{infra → ports}/clock.rb +1 -1
  73. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  74. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  75. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  76. data/lib/textus/ports/sentinel_store.rb +67 -0
  77. data/lib/textus/ports/storage/file_stat.rb +19 -0
  78. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  79. data/lib/textus/projection.rb +91 -0
  80. data/lib/textus/read/audit.rb +111 -0
  81. data/lib/textus/read/blame.rb +81 -0
  82. data/lib/textus/read/boot.rb +18 -0
  83. data/lib/textus/read/deps.rb +24 -0
  84. data/lib/textus/read/doctor.rb +19 -0
  85. data/lib/textus/read/freshness.rb +101 -0
  86. data/lib/textus/read/get.rb +66 -0
  87. data/lib/textus/read/get_or_refresh.rb +69 -0
  88. data/lib/textus/read/list.rb +15 -0
  89. data/lib/textus/read/policy_explain.rb +42 -0
  90. data/lib/textus/read/published.rb +15 -0
  91. data/lib/textus/read/pulse.rb +89 -0
  92. data/lib/textus/read/rdeps.rb +25 -0
  93. data/lib/textus/read/retainable.rb +17 -0
  94. data/lib/textus/read/schema_envelope.rb +16 -0
  95. data/lib/textus/read/stale.rb +17 -0
  96. data/lib/textus/read/uid.rb +20 -0
  97. data/lib/textus/read/validate_all.rb +22 -0
  98. data/lib/textus/read/validator.rb +84 -0
  99. data/lib/textus/read/where.rb +16 -0
  100. data/lib/textus/role_scope.rb +50 -0
  101. data/lib/textus/schema/tools.rb +3 -3
  102. data/lib/textus/store.rb +16 -7
  103. data/lib/textus/version.rb +1 -1
  104. data/lib/textus/write/accept.rb +86 -0
  105. data/lib/textus/write/authority_gate.rb +24 -0
  106. data/lib/textus/write/delete.rb +40 -0
  107. data/lib/textus/write/intake_fetch.rb +23 -0
  108. data/lib/textus/write/materializer.rb +48 -0
  109. data/lib/textus/write/mv.rb +113 -0
  110. data/lib/textus/write/publish.rb +66 -0
  111. data/lib/textus/write/put.rb +45 -0
  112. data/lib/textus/write/refresh_all.rb +44 -0
  113. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  114. data/lib/textus/write/refresh_worker.rb +124 -0
  115. data/lib/textus/write/reject.rb +54 -0
  116. data/lib/textus/write/retention_sweep.rb +55 -0
  117. data/lib/textus.rb +1 -2
  118. metadata +62 -50
  119. data/lib/textus/application/caps.rb +0 -49
  120. data/lib/textus/application/context.rb +0 -34
  121. data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
  122. data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
  123. data/lib/textus/application/maintenance/migrate.rb +0 -59
  124. data/lib/textus/application/maintenance/rule_lint.rb +0 -65
  125. data/lib/textus/application/maintenance/zone_mv.rb +0 -60
  126. data/lib/textus/application/maintenance.rb +0 -17
  127. data/lib/textus/application/projection.rb +0 -93
  128. data/lib/textus/application/read/audit.rb +0 -106
  129. data/lib/textus/application/read/blame.rb +0 -91
  130. data/lib/textus/application/read/deps.rb +0 -34
  131. data/lib/textus/application/read/freshness.rb +0 -110
  132. data/lib/textus/application/read/get.rb +0 -75
  133. data/lib/textus/application/read/get_or_refresh.rb +0 -63
  134. data/lib/textus/application/read/list.rb +0 -25
  135. data/lib/textus/application/read/policy_explain.rb +0 -47
  136. data/lib/textus/application/read/published.rb +0 -25
  137. data/lib/textus/application/read/pulse.rb +0 -101
  138. data/lib/textus/application/read/rdeps.rb +0 -35
  139. data/lib/textus/application/read/schema_envelope.rb +0 -26
  140. data/lib/textus/application/read/stale.rb +0 -23
  141. data/lib/textus/application/read/uid.rb +0 -30
  142. data/lib/textus/application/read/validate_all.rb +0 -32
  143. data/lib/textus/application/read/validator.rb +0 -86
  144. data/lib/textus/application/read/where.rb +0 -26
  145. data/lib/textus/application/use_case.rb +0 -22
  146. data/lib/textus/application/write/accept.rb +0 -102
  147. data/lib/textus/application/write/authority_gate.rb +0 -26
  148. data/lib/textus/application/write/delete.rb +0 -45
  149. data/lib/textus/application/write/materializer.rb +0 -49
  150. data/lib/textus/application/write/mv.rb +0 -118
  151. data/lib/textus/application/write/publish.rb +0 -96
  152. data/lib/textus/application/write/put.rb +0 -49
  153. data/lib/textus/application/write/refresh_all.rb +0 -63
  154. data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
  155. data/lib/textus/application/write/refresh_worker.rb +0 -134
  156. data/lib/textus/application/write/reject.rb +0 -62
  157. data/lib/textus/session.rb +0 -84
@@ -0,0 +1,55 @@
1
+ require "fileutils"
2
+
3
+ module Textus
4
+ module Write
5
+ # Applies retention actions reported by Read::Retainable. `expire` deletes
6
+ # the leaf through the role gate; `archive` copies it to
7
+ # <root>/archive/<relative-path> first, then deletes. Rows whose zone the
8
+ # caller's role cannot write surface in `failed` rather than aborting.
9
+ class RetentionSweep
10
+ def initialize(container:, call:)
11
+ @container = container
12
+ @call = call
13
+ end
14
+
15
+ def call(prefix: nil, zone: nil)
16
+ rows = Textus::Read::Retainable.new(container: @container, call: @call)
17
+ .call(prefix: prefix, zone: zone)
18
+ delete_op = Textus::Write::Delete.new(container: @container, call: @call)
19
+ expired = []
20
+ archived = []
21
+ failed = []
22
+
23
+ rows.each do |row|
24
+ key = row["key"]
25
+ begin
26
+ archive_leaf(row) if row["action"] == "archive"
27
+ delete_op.call(key)
28
+ (row["action"] == "archive" ? archived : expired) << key
29
+ rescue Textus::Error => e
30
+ failed << { "key" => key, "error" => e.message }
31
+ end
32
+ end
33
+
34
+ {
35
+ "protocol" => Textus::PROTOCOL,
36
+ "ok" => failed.empty?,
37
+ "expired" => expired,
38
+ "archived" => archived,
39
+ "failed" => failed,
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def archive_leaf(row)
46
+ src = row["path"]
47
+ root = @container.root.to_s
48
+ rel = src.delete_prefix("#{root}/")
49
+ dest = File.join(root, "archive", rel)
50
+ FileUtils.mkdir_p(File.dirname(dest))
51
+ FileUtils.cp(src, dest)
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/textus.rb CHANGED
@@ -10,16 +10,15 @@ loader.inflector.inflect(
10
10
  "json" => "Json",
11
11
  "yaml" => "Yaml",
12
12
  "hook_dsl_scanner" => "HookDSLScanner",
13
+ "io" => "IO",
13
14
  "mcp" => "MCP",
14
15
  "mcp_serve" => "MCPServe",
15
16
  )
16
17
  loader.ignore(File.expand_path("textus/errors.rb", __dir__))
17
18
  loader.ignore(File.expand_path("textus/mcp.rb", __dir__))
18
19
  loader.ignore(File.expand_path("textus/mcp/errors.rb", __dir__))
19
- loader.ignore(File.expand_path("textus/session.rb", __dir__))
20
20
  loader.setup
21
21
  loader.eager_load
22
- require_relative "textus/session"
23
22
 
24
23
  module Textus
25
24
  @hook_mutex = Mutex.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -109,46 +109,6 @@ files:
109
109
  - docs/conventions.md
110
110
  - exe/textus
111
111
  - lib/textus.rb
112
- - lib/textus/application/caps.rb
113
- - lib/textus/application/context.rb
114
- - lib/textus/application/envelope/reader.rb
115
- - lib/textus/application/envelope/writer.rb
116
- - lib/textus/application/maintenance.rb
117
- - lib/textus/application/maintenance/key_delete_prefix.rb
118
- - lib/textus/application/maintenance/key_mv_prefix.rb
119
- - lib/textus/application/maintenance/migrate.rb
120
- - lib/textus/application/maintenance/rule_lint.rb
121
- - lib/textus/application/maintenance/zone_mv.rb
122
- - lib/textus/application/projection.rb
123
- - lib/textus/application/read/audit.rb
124
- - lib/textus/application/read/blame.rb
125
- - lib/textus/application/read/deps.rb
126
- - lib/textus/application/read/freshness.rb
127
- - lib/textus/application/read/get.rb
128
- - lib/textus/application/read/get_or_refresh.rb
129
- - lib/textus/application/read/list.rb
130
- - lib/textus/application/read/policy_explain.rb
131
- - lib/textus/application/read/published.rb
132
- - lib/textus/application/read/pulse.rb
133
- - lib/textus/application/read/rdeps.rb
134
- - lib/textus/application/read/schema_envelope.rb
135
- - lib/textus/application/read/stale.rb
136
- - lib/textus/application/read/uid.rb
137
- - lib/textus/application/read/validate_all.rb
138
- - lib/textus/application/read/validator.rb
139
- - lib/textus/application/read/where.rb
140
- - lib/textus/application/use_case.rb
141
- - lib/textus/application/write/accept.rb
142
- - lib/textus/application/write/authority_gate.rb
143
- - lib/textus/application/write/delete.rb
144
- - lib/textus/application/write/materializer.rb
145
- - lib/textus/application/write/mv.rb
146
- - lib/textus/application/write/publish.rb
147
- - lib/textus/application/write/put.rb
148
- - lib/textus/application/write/refresh_all.rb
149
- - lib/textus/application/write/refresh_orchestrator.rb
150
- - lib/textus/application/write/refresh_worker.rb
151
- - lib/textus/application/write/reject.rb
152
112
  - lib/textus/boot.rb
153
113
  - lib/textus/builder/pipeline.rb
154
114
  - lib/textus/builder/renderer.rb
@@ -156,6 +116,7 @@ files:
156
116
  - lib/textus/builder/renderer/markdown.rb
157
117
  - lib/textus/builder/renderer/text.rb
158
118
  - lib/textus/builder/renderer/yaml.rb
119
+ - lib/textus/call.rb
159
120
  - lib/textus/cli.rb
160
121
  - lib/textus/cli/group.rb
161
122
  - lib/textus/cli/group/hook.rb
@@ -191,6 +152,7 @@ files:
191
152
  - lib/textus/cli/verb/refresh.rb
192
153
  - lib/textus/cli/verb/refresh_stale.rb
193
154
  - lib/textus/cli/verb/reject.rb
155
+ - lib/textus/cli/verb/retain.rb
194
156
  - lib/textus/cli/verb/rule_explain.rb
195
157
  - lib/textus/cli/verb/rule_lint.rb
196
158
  - lib/textus/cli/verb/rule_list.rb
@@ -201,6 +163,8 @@ files:
201
163
  - lib/textus/cli/verb/uid.rb
202
164
  - lib/textus/cli/verb/where.rb
203
165
  - lib/textus/cli/verb/zone_mv.rb
166
+ - lib/textus/container.rb
167
+ - lib/textus/dispatcher.rb
204
168
  - lib/textus/doctor.rb
205
169
  - lib/textus/doctor/check.rb
206
170
  - lib/textus/doctor/check/audit_log.rb
@@ -220,6 +184,7 @@ files:
220
184
  - lib/textus/doctor/check/unowned_schema_fields.rb
221
185
  - lib/textus/domain/action.rb
222
186
  - lib/textus/domain/authorizer.rb
187
+ - lib/textus/domain/duration.rb
223
188
  - lib/textus/domain/freshness.rb
224
189
  - lib/textus/domain/freshness/evaluator.rb
225
190
  - lib/textus/domain/freshness/policy.rb
@@ -233,6 +198,8 @@ files:
233
198
  - lib/textus/domain/policy/promote.rb
234
199
  - lib/textus/domain/policy/promotion.rb
235
200
  - lib/textus/domain/policy/refresh.rb
201
+ - lib/textus/domain/policy/retention.rb
202
+ - lib/textus/domain/retention.rb
236
203
  - lib/textus/domain/sentinel.rb
237
204
  - lib/textus/domain/staleness.rb
238
205
  - lib/textus/domain/staleness/generator_check.rb
@@ -244,6 +211,8 @@ files:
244
211
  - lib/textus/entry/text.rb
245
212
  - lib/textus/entry/yaml.rb
246
213
  - lib/textus/envelope.rb
214
+ - lib/textus/envelope/io/reader.rb
215
+ - lib/textus/envelope/io/writer.rb
247
216
  - lib/textus/errors.rb
248
217
  - lib/textus/etag.rb
249
218
  - lib/textus/hooks/builtin.rb
@@ -253,18 +222,17 @@ files:
253
222
  - lib/textus/hooks/fire_report.rb
254
223
  - lib/textus/hooks/loader.rb
255
224
  - lib/textus/hooks/rpc_registry.rb
256
- - lib/textus/infra/audit_log.rb
257
- - lib/textus/infra/audit_subscriber.rb
258
- - lib/textus/infra/build_lock.rb
259
- - lib/textus/infra/clock.rb
260
- - lib/textus/infra/publisher.rb
261
- - lib/textus/infra/refresh/detached.rb
262
- - lib/textus/infra/refresh/lock.rb
263
- - lib/textus/infra/storage/file_store.rb
225
+ - lib/textus/hooks/signature.rb
264
226
  - lib/textus/init.rb
265
227
  - lib/textus/key/distance.rb
266
228
  - lib/textus/key/grammar.rb
267
229
  - lib/textus/key/path.rb
230
+ - lib/textus/maintenance.rb
231
+ - lib/textus/maintenance/key_delete_prefix.rb
232
+ - lib/textus/maintenance/key_mv_prefix.rb
233
+ - lib/textus/maintenance/migrate.rb
234
+ - lib/textus/maintenance/rule_lint.rb
235
+ - lib/textus/maintenance/zone_mv.rb
268
236
  - lib/textus/manifest.rb
269
237
  - lib/textus/manifest/data.rb
270
238
  - lib/textus/manifest/entry.rb
@@ -292,14 +260,58 @@ files:
292
260
  - lib/textus/mcp/tool_schemas.rb
293
261
  - lib/textus/mcp/tools.rb
294
262
  - lib/textus/mustache.rb
263
+ - lib/textus/ports/audit_log.rb
264
+ - lib/textus/ports/audit_subscriber.rb
265
+ - lib/textus/ports/build_lock.rb
266
+ - lib/textus/ports/clock.rb
267
+ - lib/textus/ports/publisher.rb
268
+ - lib/textus/ports/refresh/detached.rb
269
+ - lib/textus/ports/refresh/lock.rb
270
+ - lib/textus/ports/sentinel_store.rb
271
+ - lib/textus/ports/storage/file_stat.rb
272
+ - lib/textus/ports/storage/file_store.rb
273
+ - lib/textus/projection.rb
274
+ - lib/textus/read/audit.rb
275
+ - lib/textus/read/blame.rb
276
+ - lib/textus/read/boot.rb
277
+ - lib/textus/read/deps.rb
278
+ - lib/textus/read/doctor.rb
279
+ - lib/textus/read/freshness.rb
280
+ - lib/textus/read/get.rb
281
+ - lib/textus/read/get_or_refresh.rb
282
+ - lib/textus/read/list.rb
283
+ - lib/textus/read/policy_explain.rb
284
+ - lib/textus/read/published.rb
285
+ - lib/textus/read/pulse.rb
286
+ - lib/textus/read/rdeps.rb
287
+ - lib/textus/read/retainable.rb
288
+ - lib/textus/read/schema_envelope.rb
289
+ - lib/textus/read/stale.rb
290
+ - lib/textus/read/uid.rb
291
+ - lib/textus/read/validate_all.rb
292
+ - lib/textus/read/validator.rb
293
+ - lib/textus/read/where.rb
295
294
  - lib/textus/role.rb
295
+ - lib/textus/role_scope.rb
296
296
  - lib/textus/schema.rb
297
297
  - lib/textus/schema/tools.rb
298
298
  - lib/textus/schemas.rb
299
- - lib/textus/session.rb
300
299
  - lib/textus/store.rb
301
300
  - lib/textus/uid.rb
302
301
  - lib/textus/version.rb
302
+ - lib/textus/write/accept.rb
303
+ - lib/textus/write/authority_gate.rb
304
+ - lib/textus/write/delete.rb
305
+ - lib/textus/write/intake_fetch.rb
306
+ - lib/textus/write/materializer.rb
307
+ - lib/textus/write/mv.rb
308
+ - lib/textus/write/publish.rb
309
+ - lib/textus/write/put.rb
310
+ - lib/textus/write/refresh_all.rb
311
+ - lib/textus/write/refresh_orchestrator.rb
312
+ - lib/textus/write/refresh_worker.rb
313
+ - lib/textus/write/reject.rb
314
+ - lib/textus/write/retention_sweep.rb
303
315
  homepage: https://github.com/patrick204nqh/textus
304
316
  licenses:
305
317
  - MIT
@@ -1,49 +0,0 @@
1
- module Textus
2
- module Application
3
- # Capability records: role-scoped slices of the Store handed to use cases.
4
- # Zeitwerk maps this file to Textus::Application::Caps; the three
5
- # concrete cap types are also promoted to the Application namespace for
6
- # concise reference (Application::ReadCaps, etc.).
7
- module Caps
8
- ReadCaps = Data.define(:manifest, :file_store, :schemas, :root, :audit_log, :events)
9
-
10
- WriteCaps = Data.define(
11
- :manifest, :file_store, :schemas, :root,
12
- :audit_log, :events, :authorizer
13
- ) do
14
- def read
15
- ReadCaps.new(
16
- manifest: manifest, file_store: file_store, schemas: schemas, root: root,
17
- audit_log: audit_log, events: events
18
- )
19
- end
20
- end
21
-
22
- HookCaps = Data.define(:events, :rpc, :manifest, :root)
23
- end
24
-
25
- # Promote to Application namespace for concise reference.
26
- ReadCaps = Caps::ReadCaps
27
- WriteCaps = Caps::WriteCaps
28
- HookCaps = Caps::HookCaps
29
-
30
- def self.caps_from_store(store)
31
- read = ReadCaps.new(
32
- manifest: store.manifest, file_store: store.file_store,
33
- schemas: store.schemas, root: store.root,
34
- audit_log: store.audit_log, events: store.events
35
- )
36
- write = WriteCaps.new(
37
- manifest: store.manifest, file_store: store.file_store,
38
- schemas: store.schemas, root: store.root,
39
- audit_log: store.audit_log, events: store.events,
40
- authorizer: Textus::Domain::Authorizer.new(manifest: store.manifest)
41
- )
42
- hook = HookCaps.new(
43
- events: store.events, rpc: store.rpc,
44
- manifest: store.manifest, root: store.root
45
- )
46
- [read, write, hook]
47
- end
48
- end
49
- end
@@ -1,34 +0,0 @@
1
- require "securerandom"
2
-
3
- module Textus
4
- module Application
5
- # A Context describes the call: who is acting (role), what request this
6
- # is part of (correlation_id), what time it is (now), and whether
7
- # writes should be suppressed (dry_run).
8
- #
9
- # Collaborators (manifest, file_store, bus, audit log, authorizer) are
10
- # never read from Context — use cases pull them from a Caps record
11
- # (Read/Write/Hook) that Session derives from the Store.
12
- Context = Data.define(:role, :correlation_id, :now, :dry_run) do
13
- def self.build(role:, correlation_id: nil, now: nil, dry_run: false)
14
- new(
15
- role: role.to_s,
16
- correlation_id: correlation_id || SecureRandom.uuid,
17
- now: now || Time.now,
18
- dry_run: dry_run,
19
- )
20
- end
21
-
22
- def dry_run? = dry_run
23
-
24
- def with_role(new_role)
25
- self.class.new(
26
- role: new_role.to_s,
27
- correlation_id: correlation_id,
28
- now: now,
29
- dry_run: dry_run,
30
- )
31
- end
32
- end
33
- end
34
- end
@@ -1,44 +0,0 @@
1
- module Textus
2
- module Application
3
- module Maintenance
4
- # Bulk-delete every leaf key under `prefix`.
5
- module KeyDeletePrefix
6
- def self.call(*, session:, ctx:, caps:, **)
7
- Impl.new(ctx: ctx, caps: caps, session: session).call(*, **)
8
- end
9
-
10
- class Impl
11
- def initialize(ctx:, caps:, session:)
12
- @ctx = ctx
13
- @caps = caps
14
- @session = session
15
- end
16
-
17
- def call(prefix:, dry_run: false)
18
- raise UsageError.new("prefix required") if prefix.nil? || prefix.empty?
19
-
20
- leaves = Read::List::Impl.new(caps: @caps)
21
- .call(prefix: prefix)
22
- .map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
23
-
24
- warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
25
- steps = leaves.map { |k| { "op" => "delete", "key" => k } }
26
-
27
- plan = Plan.new(steps: steps, warnings: warnings)
28
- return plan if dry_run
29
-
30
- steps.each do |s|
31
- Textus::Application::Write::Delete.call(
32
- s["key"],
33
- session: @session, ctx: @ctx, caps: @session.write_caps,
34
- )
35
- end
36
- plan
37
- end
38
- end
39
- end
40
- end
41
- end
42
- end
43
-
44
- Textus::Application::UseCase.register(:key_delete_prefix, Textus::Application::Maintenance::KeyDeletePrefix, caps: :write)
@@ -1,57 +0,0 @@
1
- module Textus
2
- module Application
3
- module Maintenance
4
- # Bulk-rename every leaf key under `from_prefix` to `to_prefix`.
5
- # Calls Write::Mv directly for each entry — emits one audit row per file moved.
6
- module KeyMvPrefix
7
- def self.call(*, session:, ctx:, caps:, **)
8
- Impl.new(ctx: ctx, caps: caps, session: session).call(*, **)
9
- end
10
-
11
- class Impl
12
- def initialize(ctx:, caps:, session:)
13
- @ctx = ctx
14
- @caps = caps
15
- @session = session
16
- end
17
-
18
- def call(from_prefix:, to_prefix:, dry_run: false)
19
- raise UsageError.new("from_prefix and to_prefix required") if from_prefix.nil? || to_prefix.nil?
20
-
21
- leaves = list_leaves_under(from_prefix)
22
- warnings = []
23
- warnings << "no keys under #{from_prefix}" if leaves.empty?
24
-
25
- steps = leaves.map do |old_key|
26
- tail = old_key.delete_prefix("#{from_prefix}.")
27
- new_key = "#{to_prefix}.#{tail}"
28
- { "op" => "mv", "from" => old_key, "to" => new_key }
29
- end
30
-
31
- plan = Plan.new(steps: steps, warnings: warnings)
32
- return plan if dry_run
33
-
34
- steps.each do |s|
35
- Textus::Application::Write::Mv.call(
36
- s["from"], s["to"],
37
- session: @session, ctx: @ctx, caps: @session.write_caps,
38
- dry_run: false
39
- )
40
- end
41
- plan
42
- end
43
-
44
- private
45
-
46
- def list_leaves_under(prefix)
47
- Read::List::Impl.new(caps: @caps)
48
- .call(prefix: prefix)
49
- .map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
50
- end
51
- end
52
- end
53
- end
54
- end
55
- end
56
-
57
- Textus::Application::UseCase.register(:key_mv_prefix, Textus::Application::Maintenance::KeyMvPrefix, caps: :write)
@@ -1,59 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Application
5
- module Maintenance
6
- # Loads a YAML migration plan and dispatches each op to the
7
- # appropriate Maintenance use case. Concatenates resulting Plans.
8
- module Migrate
9
- def self.call(*, session:, ctx:, caps:, **)
10
- Impl.new(ctx: ctx, caps: caps, session: session).call(*, **)
11
- end
12
-
13
- class Impl
14
- def initialize(ctx:, caps:, session:)
15
- @ctx = ctx
16
- @caps = caps
17
- @session = session
18
- end
19
-
20
- def call(plan_yaml:, dry_run: false)
21
- raw = YAML.safe_load(plan_yaml, permitted_classes: [Symbol], aliases: false)
22
- raise UsageError.new("migration plan must be a YAML mapping") unless raw.is_a?(Hash)
23
-
24
- ops = Array(raw["operations"])
25
- all_steps = []
26
- warnings = []
27
-
28
- ops.each do |op_hash|
29
- op_name = op_hash["op"]
30
- sub_plan = invoke_op(op_name, op_hash, dry_run: dry_run)
31
- all_steps.concat(sub_plan.steps)
32
- warnings.concat(sub_plan.warnings)
33
- end
34
-
35
- Plan.new(steps: all_steps, warnings: warnings)
36
- end
37
-
38
- private
39
-
40
- def invoke_op(op_name, op_hash, dry_run:)
41
- kwargs = op_hash.except("op").transform_keys(&:to_sym).merge(dry_run: dry_run)
42
- case op_name
43
- when "key_mv_prefix"
44
- KeyMvPrefix.call(session: @session, ctx: @ctx, caps: @caps, **kwargs)
45
- when "key_delete_prefix"
46
- KeyDeletePrefix.call(session: @session, ctx: @ctx, caps: @caps, **kwargs)
47
- when "zone_mv"
48
- ZoneMv.call(session: @session, ctx: @ctx, caps: @caps, **kwargs)
49
- else
50
- raise UsageError.new("unknown op: #{op_name}")
51
- end
52
- end
53
- end
54
- end
55
- end
56
- end
57
- end
58
-
59
- Textus::Application::UseCase.register(:migrate, Textus::Application::Maintenance::Migrate, caps: :write)
@@ -1,65 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Application
5
- module Maintenance
6
- # Compare the live manifest's `rules:` block against a candidate
7
- # YAML string. Returns a Plan describing rule additions/removals/
8
- # changes. Does NOT write anything.
9
- module RuleLint
10
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
11
- Impl.new(ctx: ctx, caps: caps).call(*, **)
12
- end
13
-
14
- class Impl
15
- def initialize(ctx:, caps:)
16
- @ctx = ctx
17
- @root = caps.root
18
- end
19
-
20
- def call(candidate_yaml:)
21
- live_rules = current_rules
22
- candidate_rules = parse_candidate(candidate_yaml)
23
-
24
- live_by_match = live_rules.to_h { |r| [r["match"], r] }
25
- candidate_by_match = candidate_rules.to_h { |r| [r["match"], r] }
26
-
27
- steps = (candidate_by_match.keys - live_by_match.keys).map do |m|
28
- { "op" => "add_rule", "match" => m, "rule" => candidate_by_match[m] }
29
- end
30
- (live_by_match.keys - candidate_by_match.keys).each do |m|
31
- steps << { "op" => "remove_rule", "match" => m }
32
- end
33
- (live_by_match.keys & candidate_by_match.keys).each do |m|
34
- next if live_by_match[m] == candidate_by_match[m]
35
-
36
- steps << { "op" => "change_rule", "match" => m,
37
- "from" => live_by_match[m], "to" => candidate_by_match[m] }
38
- end
39
-
40
- Plan.new(steps: steps, warnings: [])
41
- end
42
-
43
- private
44
-
45
- def current_rules
46
- raw = YAML.safe_load_file(File.join(@root, "manifest.yaml"),
47
- permitted_classes: [Symbol], aliases: false)
48
- Array(raw["rules"])
49
- end
50
-
51
- def parse_candidate(yaml_text)
52
- raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
53
- raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
54
-
55
- Array(raw["rules"])
56
- rescue Psych::Exception => e
57
- raise UsageError.new("candidate YAML parse error: #{e.message}")
58
- end
59
- end
60
- end
61
- end
62
- end
63
- end
64
-
65
- Textus::Application::UseCase.register(:rule_lint, Textus::Application::Maintenance::RuleLint, caps: :read)
@@ -1,60 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Application
5
- module Maintenance
6
- # Rename a zone — rewrites the manifest's zones[] entry, rewrites
7
- # the `zone:` field on every entry under the old zone, and moves
8
- # every file from zones/<old>/ to zones/<new>/.
9
- module ZoneMv
10
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
11
- Impl.new(ctx: ctx, caps: caps).call(*, **)
12
- end
13
-
14
- class Impl
15
- def initialize(ctx:, caps:)
16
- @ctx = ctx
17
- @manifest = caps.manifest
18
- @root = caps.root
19
- end
20
-
21
- def call(from:, to:, dry_run: false)
22
- raise UsageError.new("from and to required") if from.nil? || to.nil? || from.empty? || to.empty?
23
- raise UsageError.new("zone '#{from}' not declared") unless @manifest.data.zones.key?(from)
24
-
25
- dest_dir = File.join(@root, "zones", to)
26
- raise UsageError.new("destination 'zones/#{to}' already exists") if File.exist?(dest_dir)
27
-
28
- affected_keys = @manifest.data.entries.select { |e| e.zone == from }.map(&:key)
29
-
30
- steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
31
- steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
32
-
33
- plan = Plan.new(steps: steps, warnings: [])
34
- return plan if dry_run
35
-
36
- rewrite_manifest!(from, to)
37
- FileUtils.mv(File.join(@root, "zones", from), dest_dir)
38
- plan
39
- end
40
-
41
- private
42
-
43
- def rewrite_manifest!(from, to)
44
- path = File.join(@root, "manifest.yaml")
45
- raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
46
- raw["zones"].each { |z| z["name"] = to if z["name"] == from }
47
- raw["entries"].each do |e|
48
- e["zone"] = to if e["zone"] == from
49
- e["key"] = e["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
50
- e["path"] = e["path"].sub(%r{\A#{Regexp.escape(from)}(/|\z)}, "#{to}\\1")
51
- end
52
- File.write(path, YAML.dump(raw))
53
- end
54
- end
55
- end
56
- end
57
- end
58
- end
59
-
60
- Textus::Application::UseCase.register(:zone_mv, Textus::Application::Maintenance::ZoneMv, caps: :write)
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Application
3
- # Bulk and structural changes to a textus store. Each use case returns
4
- # a Plan when called with dry_run: true, and applies the plan when
5
- # called with dry_run: false.
6
- module Maintenance
7
- # A Plan is a JSON-shaped preview. Steps are op-tagged hashes the
8
- # use case knows how to apply. Warnings are strings surfaced to
9
- # the operator (skipped keys, ambiguities).
10
- Plan = Data.define(:steps, :warnings) do
11
- def to_h
12
- { "steps" => steps, "warnings" => warnings }
13
- end
14
- end
15
- end
16
- end
17
- end