textus 0.26.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +111 -67
  3. data/CHANGELOG.md +76 -0
  4. data/README.md +55 -13
  5. data/SPEC.md +75 -38
  6. data/docs/conventions.md +4 -4
  7. data/lib/textus/boot.rb +14 -10
  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 -2
  15. data/lib/textus/cli/verb/put.rb +3 -3
  16. data/lib/textus/cli/verb.rb +6 -6
  17. data/lib/textus/cli.rb +0 -7
  18. data/lib/textus/container.rb +23 -0
  19. data/lib/textus/dispatcher.rb +49 -0
  20. data/lib/textus/doctor/check/audit_log.rb +1 -1
  21. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  22. data/lib/textus/doctor/check/sentinels.rb +10 -8
  23. data/lib/textus/doctor/check.rb +12 -5
  24. data/lib/textus/doctor.rb +7 -7
  25. data/lib/textus/domain/authorizer.rb +2 -2
  26. data/lib/textus/domain/sentinel.rb +9 -65
  27. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  28. data/lib/textus/domain/staleness/intake_check.rb +18 -10
  29. data/lib/textus/domain/staleness.rb +3 -3
  30. data/lib/textus/{application/envelope → envelope/io}/reader.rb +2 -2
  31. data/lib/textus/{application/envelope → envelope/io}/writer.rb +11 -11
  32. data/lib/textus/hooks/context.rb +30 -13
  33. data/lib/textus/hooks/rpc_registry.rb +1 -1
  34. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  35. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  36. data/lib/textus/maintenance/migrate.rb +51 -0
  37. data/lib/textus/maintenance/rule_lint.rb +56 -0
  38. data/lib/textus/maintenance/zone_mv.rb +51 -0
  39. data/lib/textus/maintenance.rb +15 -0
  40. data/lib/textus/manifest/data.rb +4 -3
  41. data/lib/textus/manifest/entry/base.rb +38 -18
  42. data/lib/textus/manifest/entry/derived.rb +6 -6
  43. data/lib/textus/manifest/entry/nested.rb +7 -9
  44. data/lib/textus/manifest/entry/parser.rb +2 -2
  45. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  46. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  47. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  48. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  49. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  50. data/lib/textus/manifest/entry/validators.rb +2 -2
  51. data/lib/textus/manifest/entry.rb +0 -5
  52. data/lib/textus/manifest.rb +1 -6
  53. data/lib/textus/mcp/server.rb +1 -2
  54. data/lib/textus/mcp/session.rb +10 -1
  55. data/lib/textus/mcp/tools.rb +2 -2
  56. data/lib/textus/mcp.rb +1 -1
  57. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  58. data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
  59. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  60. data/lib/textus/{infra → ports}/clock.rb +1 -1
  61. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  62. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  63. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  64. data/lib/textus/ports/sentinel_store.rb +67 -0
  65. data/lib/textus/ports/storage/file_stat.rb +19 -0
  66. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  67. data/lib/textus/projection.rb +91 -0
  68. data/lib/textus/read/audit.rb +111 -0
  69. data/lib/textus/read/blame.rb +81 -0
  70. data/lib/textus/read/boot.rb +18 -0
  71. data/lib/textus/read/deps.rb +24 -0
  72. data/lib/textus/read/doctor.rb +19 -0
  73. data/lib/textus/read/freshness.rb +101 -0
  74. data/lib/textus/read/get.rb +66 -0
  75. data/lib/textus/read/get_or_refresh.rb +69 -0
  76. data/lib/textus/read/list.rb +15 -0
  77. data/lib/textus/read/policy_explain.rb +37 -0
  78. data/lib/textus/read/published.rb +15 -0
  79. data/lib/textus/read/pulse.rb +89 -0
  80. data/lib/textus/read/rdeps.rb +25 -0
  81. data/lib/textus/read/schema_envelope.rb +16 -0
  82. data/lib/textus/read/stale.rb +17 -0
  83. data/lib/textus/read/uid.rb +20 -0
  84. data/lib/textus/read/validate_all.rb +22 -0
  85. data/lib/textus/read/validator.rb +84 -0
  86. data/lib/textus/read/where.rb +16 -0
  87. data/lib/textus/role_scope.rb +49 -0
  88. data/lib/textus/schema/tools.rb +3 -3
  89. data/lib/textus/store.rb +16 -7
  90. data/lib/textus/version.rb +1 -1
  91. data/lib/textus/write/accept.rb +86 -0
  92. data/lib/textus/write/authority_gate.rb +24 -0
  93. data/lib/textus/write/delete.rb +54 -0
  94. data/lib/textus/write/materializer.rb +48 -0
  95. data/lib/textus/write/mv.rb +123 -0
  96. data/lib/textus/write/publish.rb +66 -0
  97. data/lib/textus/write/put.rb +59 -0
  98. data/lib/textus/write/refresh_all.rb +44 -0
  99. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  100. data/lib/textus/write/refresh_worker.rb +138 -0
  101. data/lib/textus/write/reject.rb +54 -0
  102. data/lib/textus.rb +1 -2
  103. metadata +54 -50
  104. data/lib/textus/application/caps.rb +0 -49
  105. data/lib/textus/application/context.rb +0 -34
  106. data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
  107. data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
  108. data/lib/textus/application/maintenance/migrate.rb +0 -59
  109. data/lib/textus/application/maintenance/rule_lint.rb +0 -65
  110. data/lib/textus/application/maintenance/zone_mv.rb +0 -60
  111. data/lib/textus/application/maintenance.rb +0 -17
  112. data/lib/textus/application/projection.rb +0 -93
  113. data/lib/textus/application/read/audit.rb +0 -106
  114. data/lib/textus/application/read/blame.rb +0 -91
  115. data/lib/textus/application/read/deps.rb +0 -34
  116. data/lib/textus/application/read/freshness.rb +0 -110
  117. data/lib/textus/application/read/get.rb +0 -75
  118. data/lib/textus/application/read/get_or_refresh.rb +0 -63
  119. data/lib/textus/application/read/list.rb +0 -25
  120. data/lib/textus/application/read/policy_explain.rb +0 -47
  121. data/lib/textus/application/read/published.rb +0 -25
  122. data/lib/textus/application/read/pulse.rb +0 -101
  123. data/lib/textus/application/read/rdeps.rb +0 -35
  124. data/lib/textus/application/read/schema_envelope.rb +0 -26
  125. data/lib/textus/application/read/stale.rb +0 -23
  126. data/lib/textus/application/read/uid.rb +0 -30
  127. data/lib/textus/application/read/validate_all.rb +0 -32
  128. data/lib/textus/application/read/validator.rb +0 -86
  129. data/lib/textus/application/read/where.rb +0 -26
  130. data/lib/textus/application/use_case.rb +0 -22
  131. data/lib/textus/application/write/accept.rb +0 -102
  132. data/lib/textus/application/write/authority_gate.rb +0 -26
  133. data/lib/textus/application/write/delete.rb +0 -45
  134. data/lib/textus/application/write/materializer.rb +0 -49
  135. data/lib/textus/application/write/mv.rb +0 -118
  136. data/lib/textus/application/write/publish.rb +0 -96
  137. data/lib/textus/application/write/put.rb +0 -49
  138. data/lib/textus/application/write/refresh_all.rb +0 -63
  139. data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
  140. data/lib/textus/application/write/refresh_worker.rb +0 -134
  141. data/lib/textus/application/write/reject.rb +0 -62
  142. data/lib/textus/session.rb +0 -84
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.29.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
@@ -201,6 +162,8 @@ files:
201
162
  - lib/textus/cli/verb/uid.rb
202
163
  - lib/textus/cli/verb/where.rb
203
164
  - lib/textus/cli/verb/zone_mv.rb
165
+ - lib/textus/container.rb
166
+ - lib/textus/dispatcher.rb
204
167
  - lib/textus/doctor.rb
205
168
  - lib/textus/doctor/check.rb
206
169
  - lib/textus/doctor/check/audit_log.rb
@@ -244,6 +207,8 @@ files:
244
207
  - lib/textus/entry/text.rb
245
208
  - lib/textus/entry/yaml.rb
246
209
  - lib/textus/envelope.rb
210
+ - lib/textus/envelope/io/reader.rb
211
+ - lib/textus/envelope/io/writer.rb
247
212
  - lib/textus/errors.rb
248
213
  - lib/textus/etag.rb
249
214
  - lib/textus/hooks/builtin.rb
@@ -253,18 +218,16 @@ files:
253
218
  - lib/textus/hooks/fire_report.rb
254
219
  - lib/textus/hooks/loader.rb
255
220
  - 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
264
221
  - lib/textus/init.rb
265
222
  - lib/textus/key/distance.rb
266
223
  - lib/textus/key/grammar.rb
267
224
  - lib/textus/key/path.rb
225
+ - lib/textus/maintenance.rb
226
+ - lib/textus/maintenance/key_delete_prefix.rb
227
+ - lib/textus/maintenance/key_mv_prefix.rb
228
+ - lib/textus/maintenance/migrate.rb
229
+ - lib/textus/maintenance/rule_lint.rb
230
+ - lib/textus/maintenance/zone_mv.rb
268
231
  - lib/textus/manifest.rb
269
232
  - lib/textus/manifest/data.rb
270
233
  - lib/textus/manifest/entry.rb
@@ -292,14 +255,55 @@ files:
292
255
  - lib/textus/mcp/tool_schemas.rb
293
256
  - lib/textus/mcp/tools.rb
294
257
  - lib/textus/mustache.rb
258
+ - lib/textus/ports/audit_log.rb
259
+ - lib/textus/ports/audit_subscriber.rb
260
+ - lib/textus/ports/build_lock.rb
261
+ - lib/textus/ports/clock.rb
262
+ - lib/textus/ports/publisher.rb
263
+ - lib/textus/ports/refresh/detached.rb
264
+ - lib/textus/ports/refresh/lock.rb
265
+ - lib/textus/ports/sentinel_store.rb
266
+ - lib/textus/ports/storage/file_stat.rb
267
+ - lib/textus/ports/storage/file_store.rb
268
+ - lib/textus/projection.rb
269
+ - lib/textus/read/audit.rb
270
+ - lib/textus/read/blame.rb
271
+ - lib/textus/read/boot.rb
272
+ - lib/textus/read/deps.rb
273
+ - lib/textus/read/doctor.rb
274
+ - lib/textus/read/freshness.rb
275
+ - lib/textus/read/get.rb
276
+ - lib/textus/read/get_or_refresh.rb
277
+ - lib/textus/read/list.rb
278
+ - lib/textus/read/policy_explain.rb
279
+ - lib/textus/read/published.rb
280
+ - lib/textus/read/pulse.rb
281
+ - lib/textus/read/rdeps.rb
282
+ - lib/textus/read/schema_envelope.rb
283
+ - lib/textus/read/stale.rb
284
+ - lib/textus/read/uid.rb
285
+ - lib/textus/read/validate_all.rb
286
+ - lib/textus/read/validator.rb
287
+ - lib/textus/read/where.rb
295
288
  - lib/textus/role.rb
289
+ - lib/textus/role_scope.rb
296
290
  - lib/textus/schema.rb
297
291
  - lib/textus/schema/tools.rb
298
292
  - lib/textus/schemas.rb
299
- - lib/textus/session.rb
300
293
  - lib/textus/store.rb
301
294
  - lib/textus/uid.rb
302
295
  - lib/textus/version.rb
296
+ - lib/textus/write/accept.rb
297
+ - lib/textus/write/authority_gate.rb
298
+ - lib/textus/write/delete.rb
299
+ - lib/textus/write/materializer.rb
300
+ - lib/textus/write/mv.rb
301
+ - lib/textus/write/publish.rb
302
+ - lib/textus/write/put.rb
303
+ - lib/textus/write/refresh_all.rb
304
+ - lib/textus/write/refresh_orchestrator.rb
305
+ - lib/textus/write/refresh_worker.rb
306
+ - lib/textus/write/reject.rb
303
307
  homepage: https://github.com/patrick204nqh/textus
304
308
  licenses:
305
309
  - 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
@@ -1,93 +0,0 @@
1
- require "time"
2
- require "timeout"
3
-
4
- module Textus
5
- module Application
6
- class Projection
7
- MAX_LIMIT = 1000
8
- REDUCER_TIMEOUT_SECONDS = 2
9
-
10
- # `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
11
- # semantics: pure read (`ops.get`) for materialization paths;
12
- # `ops.get_or_refresh` if you want refresh-on-stale.
13
- # `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
14
- # `rpc` — a `Hooks::RpcRegistry` used to dispatch `transform_rows` callables.
15
- # `transform_context` — capability object handed to transform reducers as `caps:`.
16
- def initialize(reader:, spec:, lister:, rpc:, transform_context:)
17
- @reader = reader
18
- @spec = spec || {}
19
- @lister = lister
20
- @rpc = rpc
21
- @transform_context = transform_context
22
- @limit = (@spec["limit"] || MAX_LIMIT).to_i
23
- raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
24
- end
25
-
26
- def run
27
- keys = collect_keys
28
- explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
29
- rows = keys.map do |key|
30
- env = @reader.call(key)
31
- row = pluck(env.meta, env.body)
32
- explicit_pluck ? row : row.merge("_key" => key)
33
- end
34
- reduced = apply_reducer(rows)
35
- # Reducers may return either an Array of rows (legacy / templated builds)
36
- # or a Hash that becomes the structured-format payload base. In the Hash
37
- # case, downstream sort/limit/position markers don't apply, and the
38
- # builder owns `_meta.generated_at` so we don't stamp it here.
39
- return reduced if reduced.is_a?(Hash)
40
-
41
- rows = reduced
42
- rows = sort(rows)
43
- rows = rows.first(@limit)
44
- mark_positions(rows)
45
- { "entries" => rows, "count" => rows.length, "generated_at" => Time.now.utc.iso8601 }
46
- end
47
-
48
- private
49
-
50
- def apply_reducer(rows)
51
- name = @spec["transform"] or return rows
52
- Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
53
- @rpc.invoke(:transform_rows, name,
54
- caps: @transform_context,
55
- rows: rows,
56
- config: @spec["transform_config"] || {})
57
- end
58
- rescue Timeout::Error
59
- raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
60
- end
61
-
62
- def collect_keys
63
- prefixes = Array(@spec["select"])
64
- prefixes.flat_map { |p| @lister.call(prefix: p).map { |row| row["key"] } }.uniq
65
- end
66
-
67
- def pluck(frontmatter, _body)
68
- fields = @spec["pluck"]
69
- if fields.nil? || fields == "*"
70
- frontmatter
71
- else
72
- Array(fields).each_with_object({}) { |f, h| h[f] = frontmatter[f] if frontmatter.key?(f) }
73
- end
74
- end
75
-
76
- # Adds `_first`, `_last`, and `_index` markers so templates can emit
77
- # delimiters (e.g. JSON commas) via {{^_last}},{{/_last}}.
78
- def mark_positions(rows)
79
- last_idx = rows.length - 1
80
- rows.each_with_index do |row, i|
81
- row["_index"] = i
82
- row["_first"] = i.zero?
83
- row["_last"] = (i == last_idx)
84
- end
85
- end
86
-
87
- def sort(rows)
88
- sb = @spec["sort_by"] or return rows
89
- rows.sort_by { |r| r[sb].to_s }
90
- end
91
- end
92
- end
93
- end