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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +111 -67
- data/CHANGELOG.md +76 -0
- data/README.md +55 -13
- data/SPEC.md +75 -38
- data/docs/conventions.md +4 -4
- data/lib/textus/boot.rb +14 -10
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/verb/audit.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +2 -2
- data/lib/textus/cli/verb/put.rb +3 -3
- data/lib/textus/cli/verb.rb +6 -6
- data/lib/textus/cli.rb +0 -7
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +49 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +10 -8
- data/lib/textus/doctor/check.rb +12 -5
- data/lib/textus/doctor.rb +7 -7
- data/lib/textus/domain/authorizer.rb +2 -2
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +18 -10
- data/lib/textus/domain/staleness.rb +3 -3
- data/lib/textus/{application/envelope → envelope/io}/reader.rb +2 -2
- data/lib/textus/{application/envelope → envelope/io}/writer.rb +11 -11
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/rpc_registry.rb +1 -1
- data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
- data/lib/textus/maintenance/migrate.rb +51 -0
- data/lib/textus/maintenance/rule_lint.rb +56 -0
- data/lib/textus/maintenance/zone_mv.rb +51 -0
- data/lib/textus/maintenance.rb +15 -0
- data/lib/textus/manifest/data.rb +4 -3
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +6 -6
- data/lib/textus/manifest/entry/nested.rb +7 -9
- data/lib/textus/manifest/entry/parser.rb +2 -2
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
- data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
- data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +2 -2
- data/lib/textus/manifest/entry.rb +0 -5
- data/lib/textus/manifest.rb +1 -6
- data/lib/textus/mcp/server.rb +1 -2
- data/lib/textus/mcp/session.rb +10 -1
- data/lib/textus/mcp/tools.rb +2 -2
- data/lib/textus/mcp.rb +1 -1
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
- data/lib/textus/{infra → ports}/build_lock.rb +1 -1
- data/lib/textus/{infra → ports}/clock.rb +1 -1
- data/lib/textus/{infra → ports}/publisher.rb +6 -6
- data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
- data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +67 -0
- data/lib/textus/ports/storage/file_stat.rb +19 -0
- data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
- data/lib/textus/projection.rb +91 -0
- data/lib/textus/read/audit.rb +111 -0
- data/lib/textus/read/blame.rb +81 -0
- data/lib/textus/read/boot.rb +18 -0
- data/lib/textus/read/deps.rb +24 -0
- data/lib/textus/read/doctor.rb +19 -0
- data/lib/textus/read/freshness.rb +101 -0
- data/lib/textus/read/get.rb +66 -0
- data/lib/textus/read/get_or_refresh.rb +69 -0
- data/lib/textus/read/list.rb +15 -0
- data/lib/textus/read/policy_explain.rb +37 -0
- data/lib/textus/read/published.rb +15 -0
- data/lib/textus/read/pulse.rb +89 -0
- data/lib/textus/read/rdeps.rb +25 -0
- data/lib/textus/read/schema_envelope.rb +16 -0
- data/lib/textus/read/stale.rb +17 -0
- data/lib/textus/read/uid.rb +20 -0
- data/lib/textus/read/validate_all.rb +22 -0
- data/lib/textus/read/validator.rb +84 -0
- data/lib/textus/read/where.rb +16 -0
- data/lib/textus/role_scope.rb +49 -0
- data/lib/textus/schema/tools.rb +3 -3
- data/lib/textus/store.rb +16 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +86 -0
- data/lib/textus/write/authority_gate.rb +24 -0
- data/lib/textus/write/delete.rb +54 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +123 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +59 -0
- data/lib/textus/write/refresh_all.rb +44 -0
- data/lib/textus/write/refresh_orchestrator.rb +102 -0
- data/lib/textus/write/refresh_worker.rb +138 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus.rb +1 -2
- metadata +54 -50
- data/lib/textus/application/caps.rb +0 -49
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
- data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
- data/lib/textus/application/maintenance/migrate.rb +0 -59
- data/lib/textus/application/maintenance/rule_lint.rb +0 -65
- data/lib/textus/application/maintenance/zone_mv.rb +0 -60
- data/lib/textus/application/maintenance.rb +0 -17
- data/lib/textus/application/projection.rb +0 -93
- data/lib/textus/application/read/audit.rb +0 -106
- data/lib/textus/application/read/blame.rb +0 -91
- data/lib/textus/application/read/deps.rb +0 -34
- data/lib/textus/application/read/freshness.rb +0 -110
- data/lib/textus/application/read/get.rb +0 -75
- data/lib/textus/application/read/get_or_refresh.rb +0 -63
- data/lib/textus/application/read/list.rb +0 -25
- data/lib/textus/application/read/policy_explain.rb +0 -47
- data/lib/textus/application/read/published.rb +0 -25
- data/lib/textus/application/read/pulse.rb +0 -101
- data/lib/textus/application/read/rdeps.rb +0 -35
- data/lib/textus/application/read/schema_envelope.rb +0 -26
- data/lib/textus/application/read/stale.rb +0 -23
- data/lib/textus/application/read/uid.rb +0 -30
- data/lib/textus/application/read/validate_all.rb +0 -32
- data/lib/textus/application/read/validator.rb +0 -86
- data/lib/textus/application/read/where.rb +0 -26
- data/lib/textus/application/use_case.rb +0 -22
- data/lib/textus/application/write/accept.rb +0 -102
- data/lib/textus/application/write/authority_gate.rb +0 -26
- data/lib/textus/application/write/delete.rb +0 -45
- data/lib/textus/application/write/materializer.rb +0 -49
- data/lib/textus/application/write/mv.rb +0 -118
- data/lib/textus/application/write/publish.rb +0 -96
- data/lib/textus/application/write/put.rb +0 -49
- data/lib/textus/application/write/refresh_all.rb +0 -63
- data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
- data/lib/textus/application/write/refresh_worker.rb +0 -134
- data/lib/textus/application/write/reject.rb +0 -62
- 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.
|
|
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
|