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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +118 -68
- data/CHANGELOG.md +132 -0
- data/README.md +61 -19
- data/SPEC.md +107 -46
- data/docs/conventions.md +4 -4
- data/lib/textus/boot.rb +18 -12
- 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 -6
- data/lib/textus/cli/verb/put.rb +5 -14
- data/lib/textus/cli/verb/retain.rb +19 -0
- data/lib/textus/cli/verb/rule_list.rb +1 -1
- data/lib/textus/cli/verb.rb +6 -6
- data/lib/textus/cli.rb +19 -23
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +57 -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 +15 -5
- data/lib/textus/doctor.rb +7 -7
- data/lib/textus/domain/authorizer.rb +2 -2
- data/lib/textus/domain/duration.rb +22 -0
- data/lib/textus/domain/policy/refresh.rb +1 -15
- data/lib/textus/domain/policy/retention.rb +26 -0
- data/lib/textus/domain/retention.rb +44 -0
- 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 +6 -2
- data/lib/textus/{application/envelope → envelope/io}/writer.rb +19 -11
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/event_bus.rb +8 -20
- data/lib/textus/hooks/rpc_registry.rb +9 -35
- data/lib/textus/hooks/signature.rb +31 -0
- data/lib/textus/init.rb +7 -6
- 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 +9 -4
- 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/policy.rb +34 -7
- data/lib/textus/manifest/rules.rb +10 -1
- data/lib/textus/manifest/schema.rb +54 -4
- data/lib/textus/manifest.rb +4 -8
- data/lib/textus/mcp/server.rb +2 -11
- data/lib/textus/mcp/session.rb +13 -20
- 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 +42 -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/retainable.rb +17 -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 +50 -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 +40 -0
- data/lib/textus/write/intake_fetch.rb +23 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +113 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +45 -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 +124 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus/write/retention_sweep.rb +55 -0
- data/lib/textus.rb +1 -2
- metadata +62 -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
|
@@ -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.
|
|
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/
|
|
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
|