textus 0.52.0 → 0.54.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/CHANGELOG.md +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/retention/apply.rb +0 -52
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Jobs
|
|
5
|
+
class Planner
|
|
6
|
+
ACTIONS_BY_TRIGGER = {
|
|
7
|
+
"convergence" => %w[materialize sweep],
|
|
8
|
+
"entry.written" => %w[materialize],
|
|
9
|
+
"entry.deleted" => %w[materialize],
|
|
10
|
+
"entry.moved" => %w[materialize],
|
|
11
|
+
"proposal.accepted" => %w[materialize],
|
|
12
|
+
"proposal.rejected" => %w[materialize],
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
SCOPE_RESOLVERS = {
|
|
16
|
+
"materialize" => :producible_keys,
|
|
17
|
+
"sweep" => :lane_keys,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.seed(container:, queue:, role:)
|
|
21
|
+
jobs = new(container: container).plan(
|
|
22
|
+
trigger: { "type" => "convergence" },
|
|
23
|
+
role: role,
|
|
24
|
+
)
|
|
25
|
+
jobs.each { |j| queue.enqueue(j) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(container:)
|
|
29
|
+
@container = container
|
|
30
|
+
@manifest = container.manifest
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def plan(trigger:, role:)
|
|
34
|
+
type = trigger["type"] || trigger[:type]
|
|
35
|
+
trigger["target"] || trigger[:target]
|
|
36
|
+
return [] if type.nil?
|
|
37
|
+
|
|
38
|
+
blocks_with_react = @manifest.rules.blocks.select(&:react)
|
|
39
|
+
if blocks_with_react.any?
|
|
40
|
+
plan_from_rules(blocks_with_react, type, role)
|
|
41
|
+
else
|
|
42
|
+
plan_from_defaults(type, role)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def plan_from_rules(blocks, type, role)
|
|
49
|
+
jobs = []
|
|
50
|
+
blocks
|
|
51
|
+
.select { |b| matches_trigger?(b.react, type) }
|
|
52
|
+
.each do |block|
|
|
53
|
+
do_action = block.react.raw["do"]
|
|
54
|
+
Array(do_action).each do |action|
|
|
55
|
+
if action == "sweep"
|
|
56
|
+
jobs << Textus::Ports::JobStore::Job.new(
|
|
57
|
+
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
58
|
+
)
|
|
59
|
+
else
|
|
60
|
+
resolver = SCOPE_RESOLVERS.fetch(action, :producible_keys)
|
|
61
|
+
keys = send(resolver, nil)
|
|
62
|
+
keys.each { |key| jobs << job(action, key, role) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
jobs
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def plan_from_defaults(type, role)
|
|
70
|
+
actions = ACTIONS_BY_TRIGGER.fetch(type, [])
|
|
71
|
+
jobs = []
|
|
72
|
+
producible_keys(nil).each { |k| jobs << job("materialize", k, role) } if actions.include?("materialize")
|
|
73
|
+
if actions.include?("sweep")
|
|
74
|
+
jobs << Textus::Ports::JobStore::Job.new(
|
|
75
|
+
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
jobs
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def matches_trigger?(react, type)
|
|
82
|
+
on = react.raw["on"]
|
|
83
|
+
Array(on).include?(type)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def job(type, key, enqueued_by)
|
|
87
|
+
Textus::Ports::JobStore::Job.new(type: type, args: { "key" => key }, enqueued_by: enqueued_by)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def producible_keys(_target)
|
|
91
|
+
@manifest.data.entries
|
|
92
|
+
.select { |e| !e.publish_tree.nil? || !e.publish_to.empty? }
|
|
93
|
+
.map(&:key)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def lane_keys(_target)
|
|
97
|
+
@manifest.data.entries.map(&:key)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Jobs
|
|
5
|
+
class Retention
|
|
6
|
+
def initialize(container:, call:)
|
|
7
|
+
@container = container
|
|
8
|
+
@call = call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(rows)
|
|
12
|
+
out = { dropped: [], archived: [], failed: [] }
|
|
13
|
+
rows.each do |row|
|
|
14
|
+
key = row["key"]
|
|
15
|
+
begin
|
|
16
|
+
case row["action"]
|
|
17
|
+
when "drop"
|
|
18
|
+
delete(key)
|
|
19
|
+
out[:dropped] << key
|
|
20
|
+
when "archive"
|
|
21
|
+
archive_leaf(row)
|
|
22
|
+
delete(key)
|
|
23
|
+
out[:archived] << key
|
|
24
|
+
end
|
|
25
|
+
rescue Textus::Error => e
|
|
26
|
+
out[:failed] << { "key" => key, "error" => e.message }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
out
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def archive_leaf(row)
|
|
35
|
+
src = row["path"]
|
|
36
|
+
root = @container.root.to_s
|
|
37
|
+
rel = src.delete_prefix("#{root}/")
|
|
38
|
+
dest = File.join(root, "archive", rel)
|
|
39
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
40
|
+
FileUtils.cp(src, dest)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(key)
|
|
44
|
+
Textus::Action::KeyDelete.new(key: key).call(container: @container, call: @call)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Jobs
|
|
3
|
+
class Sweep < Base
|
|
4
|
+
REQUIRED_ROLE = Textus::Role::AUTOMATION
|
|
5
|
+
TYPE = "sweep"
|
|
6
|
+
|
|
7
|
+
def initialize(scope: nil, key: nil)
|
|
8
|
+
super()
|
|
9
|
+
@scope = scope || {}
|
|
10
|
+
@key = key
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def args = { scope: @scope, key: @key }.compact
|
|
14
|
+
|
|
15
|
+
def call(container:, call:)
|
|
16
|
+
prefix = @key || (@scope.is_a?(Hash) ? @scope["prefix"] : nil)
|
|
17
|
+
lane = @scope.is_a?(Hash) ? @scope["lane"] : nil
|
|
18
|
+
rows = Textus::Core::Retention::Sweep.new(
|
|
19
|
+
manifest: container.manifest,
|
|
20
|
+
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
21
|
+
clock: Textus::Ports::Clock.new,
|
|
22
|
+
).call(prefix: prefix, lane: lane)
|
|
23
|
+
Textus::Jobs::Retention.new(container: container, call: call).call(rows)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Jobs
|
|
3
|
+
class Worker
|
|
4
|
+
Summary = Struct.new(:completed, :failed, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
def self.for(container:, queue:)
|
|
7
|
+
new(queue: queue, container: container,
|
|
8
|
+
lease_ttl: container.manifest.data.worker_config[:lease_ttl])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(queue:, container:, lease_ttl: 60)
|
|
12
|
+
@queue = queue
|
|
13
|
+
@container = container
|
|
14
|
+
@lease_ttl = lease_ttl
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def drain(worker_id: "drain-#{Process.pid}")
|
|
18
|
+
completed = 0
|
|
19
|
+
failed = 0
|
|
20
|
+
loop do
|
|
21
|
+
leased = @queue.lease(worker_id: worker_id, lease_ttl: @lease_ttl)
|
|
22
|
+
break unless leased
|
|
23
|
+
|
|
24
|
+
case run_one(leased)
|
|
25
|
+
when :completed then completed += 1
|
|
26
|
+
when :dead_lettered then failed += 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
Summary.new(completed: completed, failed: failed)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def drain_pool(pool: 4)
|
|
33
|
+
summaries = []
|
|
34
|
+
mutex = Mutex.new
|
|
35
|
+
threads = Array.new(pool) do |i|
|
|
36
|
+
Thread.new do
|
|
37
|
+
s = drain(worker_id: "pool-#{Process.pid}-#{i}")
|
|
38
|
+
mutex.synchronize { summaries << s }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
threads.each(&:join)
|
|
42
|
+
Summary.new(completed: summaries.sum(&:completed), failed: summaries.sum(&:failed))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def run_one(leased)
|
|
48
|
+
job = leased.job
|
|
49
|
+
klass = Textus::Jobs.fetch(job.type)
|
|
50
|
+
action = if klass.instance_method(:initialize).parameters.any?
|
|
51
|
+
klass.new(**job.args.transform_keys(&:to_sym))
|
|
52
|
+
else
|
|
53
|
+
klass.new
|
|
54
|
+
end
|
|
55
|
+
call = Textus::Call.build(
|
|
56
|
+
role: job.enqueued_by || Textus::Role::AUTOMATION,
|
|
57
|
+
correlation_id: SecureRandom.uuid,
|
|
58
|
+
)
|
|
59
|
+
action.call(container: @container, call: call)
|
|
60
|
+
@queue.ack(leased)
|
|
61
|
+
:completed
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
@queue.fail(leased, error: e.message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/textus/jobs.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Jobs
|
|
3
|
+
@registry = {}
|
|
4
|
+
|
|
5
|
+
def self.registry = @registry
|
|
6
|
+
|
|
7
|
+
def self.register(klass)
|
|
8
|
+
@registry[klass::TYPE] = klass if klass.const_defined?(:TYPE, false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.fetch(type)
|
|
12
|
+
@registry.fetch(type) { raise Textus::UsageError.new("unknown job type: #{type}") }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/textus/key/path.rb
CHANGED
|
@@ -9,13 +9,20 @@ module Textus
|
|
|
9
9
|
# callers historically passed the whole Manifest but should now pass
|
|
10
10
|
# `manifest.data`.
|
|
11
11
|
def self.resolve(data, mentry)
|
|
12
|
-
primary_ext =
|
|
12
|
+
primary_ext = Format.for(mentry.format).extensions.first
|
|
13
|
+
rel_path = normalize_relative_path(mentry.path)
|
|
13
14
|
if File.extname(mentry.path) == ""
|
|
14
|
-
File.join(data.root,
|
|
15
|
+
File.join(data.root, rel_path + primary_ext)
|
|
15
16
|
else
|
|
16
|
-
File.join(data.root,
|
|
17
|
+
File.join(data.root, rel_path)
|
|
17
18
|
end
|
|
18
19
|
end
|
|
20
|
+
|
|
21
|
+
def self.normalize_relative_path(path)
|
|
22
|
+
return path if path.start_with?("data/")
|
|
23
|
+
|
|
24
|
+
File.join("data", path)
|
|
25
|
+
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
data/lib/textus/layout.rb
CHANGED
|
@@ -4,6 +4,15 @@ module Textus
|
|
|
4
4
|
# tracked/disposable boundary is a directory boundary. ADR 0038.
|
|
5
5
|
module Layout
|
|
6
6
|
RUN = ".run"
|
|
7
|
+
DATA = "data"
|
|
8
|
+
|
|
9
|
+
def self.data(root)
|
|
10
|
+
File.join(root, DATA)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.data_lane(root, lane_name)
|
|
14
|
+
File.join(data(root), lane_name)
|
|
15
|
+
end
|
|
7
16
|
|
|
8
17
|
def self.run(root)
|
|
9
18
|
File.join(root, RUN)
|
|
@@ -25,6 +34,10 @@ module Textus
|
|
|
25
34
|
File.join(run(root), "build.lock")
|
|
26
35
|
end
|
|
27
36
|
|
|
37
|
+
def self.watcher_lock(root)
|
|
38
|
+
File.join(run(root), "watcher.lock")
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
def self.queue(root)
|
|
29
42
|
File.join(run(root), "queue")
|
|
30
43
|
end
|
data/lib/textus/manifest/data.rb
CHANGED
|
@@ -5,15 +5,15 @@ module Textus
|
|
|
5
5
|
class Manifest
|
|
6
6
|
# Immutable, parsed view of a manifest YAML document.
|
|
7
7
|
#
|
|
8
|
-
# Holds raw structural data (
|
|
9
|
-
# but no behaviour beyond accessors. Behaviour (
|
|
8
|
+
# Holds raw structural data (lanes, entries, audit_config, role_caps)
|
|
9
|
+
# but no behaviour beyond accessors. Behaviour (lane authority, key
|
|
10
10
|
# resolution, rules) lives on Manifest::Policy / Resolver / Rules.
|
|
11
11
|
class Data
|
|
12
12
|
AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 5 }.freeze
|
|
13
13
|
WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
|
|
14
14
|
|
|
15
|
-
attr_reader :raw, :root, :entries, :
|
|
16
|
-
:
|
|
15
|
+
attr_reader :raw, :root, :entries, :declared_lane_kinds,
|
|
16
|
+
:lane_descs, :lane_owners,
|
|
17
17
|
:audit_config, :worker_config, :role_caps, :policy
|
|
18
18
|
|
|
19
19
|
def self.validate_key!(key)
|
|
@@ -27,7 +27,7 @@ module Textus
|
|
|
27
27
|
def validate_key!(key) = self.class.validate_key!(key)
|
|
28
28
|
|
|
29
29
|
def self.parse(raw, root:)
|
|
30
|
-
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare
|
|
30
|
+
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
31
31
|
|
|
32
32
|
Schema.validate!(raw)
|
|
33
33
|
new(raw: raw, root: root)
|
|
@@ -36,17 +36,17 @@ module Textus
|
|
|
36
36
|
def initialize(raw:, root:)
|
|
37
37
|
@raw = raw
|
|
38
38
|
@root = root
|
|
39
|
-
# Write authority is derived from capabilities ×
|
|
40
|
-
# not a per-
|
|
41
|
-
# one kind-keyed map below (
|
|
42
|
-
# read-side callers (boot, maintenance/
|
|
43
|
-
@
|
|
39
|
+
# Write authority is derived from capabilities × lane-kind (ADR 0030),
|
|
40
|
+
# not a per-lane writer list. "Which lanes are declared" lives in the
|
|
41
|
+
# one kind-keyed map below (declared_lane_kinds); membership checks by
|
|
42
|
+
# read-side callers (boot, maintenance/data_mv) use its keyset (ADR 0034).
|
|
43
|
+
@declared_lane_kinds = Array(raw["lanes"]).to_h do |z|
|
|
44
44
|
[z["name"], z["kind"]&.to_sym]
|
|
45
45
|
end
|
|
46
|
-
@
|
|
47
|
-
# Only
|
|
48
|
-
# future `
|
|
49
|
-
@
|
|
46
|
+
@lane_descs = Array(raw["lanes"]).to_h { |z| [z["name"], z["desc"]] }
|
|
47
|
+
# Only lanes that actually declare an owner — keep nil-tombstones out so a
|
|
48
|
+
# future `lane_owners.key?(name)` means "owner declared", not "lane exists".
|
|
49
|
+
@lane_owners = Array(raw["lanes"]).to_h { |z| [z["name"], z["owner"]] }.compact
|
|
50
50
|
@audit_config = build_audit_config(raw)
|
|
51
51
|
@worker_config = build_worker_config(raw)
|
|
52
52
|
@role_caps = Capabilities.resolve(raw["roles"])
|
|
@@ -2,14 +2,14 @@ module Textus
|
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
4
|
class Base < Entry
|
|
5
|
-
attr_reader :raw, :key, :path, :
|
|
5
|
+
attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
|
|
6
6
|
|
|
7
7
|
# rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
|
|
8
|
-
def initialize(raw:, key:, path:,
|
|
8
|
+
def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
|
|
9
9
|
@raw = raw
|
|
10
10
|
@key = key
|
|
11
11
|
@path = path
|
|
12
|
-
@
|
|
12
|
+
@lane = lane
|
|
13
13
|
@schema = schema
|
|
14
14
|
@owner = owner
|
|
15
15
|
@format = format
|
|
@@ -17,13 +17,14 @@ module Textus
|
|
|
17
17
|
end
|
|
18
18
|
# rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
policy.
|
|
20
|
+
def lane_writers(policy)
|
|
21
|
+
verb = policy.verb_for_lane(@lane)
|
|
22
|
+
policy.roles_with_capability(verb)
|
|
22
23
|
rescue UsageError => e
|
|
23
24
|
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def
|
|
27
|
+
def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
|
|
27
28
|
|
|
28
29
|
def nested? = false
|
|
29
30
|
def derived? = false
|
|
@@ -36,6 +37,9 @@ module Textus
|
|
|
36
37
|
def external? = false
|
|
37
38
|
def projection? = false
|
|
38
39
|
|
|
40
|
+
alias zone lane
|
|
41
|
+
alias in_proposal_zone? in_proposal_lane?
|
|
42
|
+
|
|
39
43
|
# Whether git should track this entry's file. Default true; an entry
|
|
40
44
|
# marked `tracked: false` in the manifest stays protocol-readable but is
|
|
41
45
|
# listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
|
|
@@ -58,22 +62,15 @@ module Textus
|
|
|
58
62
|
def ignored?(_rel_path) = false
|
|
59
63
|
|
|
60
64
|
# Minimal context object passed into entry `publish_via` hooks.
|
|
61
|
-
# Everything beyond the three primitives is derived.
|
|
62
|
-
# instances are frozen, so we recompute per-call rather than
|
|
63
|
-
# memoizing — RoleScope/Hooks::Context construction is cheap.
|
|
65
|
+
# Everything beyond the three primitives is derived.
|
|
64
66
|
PublishContext = ::Data.define(:container, :call, :reader) do
|
|
65
67
|
def manifest = container.manifest
|
|
66
68
|
def root = container.root
|
|
67
69
|
def repo_root = File.dirname(container.root)
|
|
68
|
-
def events = container.events
|
|
69
|
-
|
|
70
|
-
def hook_context
|
|
71
|
-
Textus::Hooks::Context.new(scope: scope_for_hooks)
|
|
72
|
-
end
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
# No-op: event bus removed in workflow redesign; callers that fire
|
|
72
|
+
# :entry_published / :entry_produced remain unchanged in the source.
|
|
73
|
+
def emit(_event, **_payload) = nil
|
|
77
74
|
|
|
78
75
|
# Read a named template from the store's templates/ directory.
|
|
79
76
|
# Raises TemplateError when the file doesn't exist.
|
|
@@ -87,14 +84,6 @@ module Textus
|
|
|
87
84
|
end
|
|
88
85
|
File.read(path)
|
|
89
86
|
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
def scope_for_hooks
|
|
94
|
-
Textus::RoleScope.new(
|
|
95
|
-
container: container, role: call.role, dry_run: call.dry_run,
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
87
|
end
|
|
99
88
|
|
|
100
89
|
# ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
|
|
@@ -4,8 +4,7 @@ module Textus
|
|
|
4
4
|
module Parser
|
|
5
5
|
def self.call(raw)
|
|
6
6
|
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
7
|
-
|
|
8
|
-
zone = raw["zone"] or raise UsageError.new("manifest entry '#{key}' missing zone")
|
|
7
|
+
lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
|
|
9
8
|
|
|
10
9
|
raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
|
|
11
10
|
kind = raw_kind.to_sym
|
|
@@ -15,11 +14,14 @@ module Textus
|
|
|
15
14
|
"the produce method is `source.from` (#{kind == :intake ? "handler" : "project|command"})",
|
|
16
15
|
)
|
|
17
16
|
end
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
explicit_path = raw["path"]
|
|
19
|
+
format = resolve_format(raw, explicit_path)
|
|
20
|
+
path = explicit_path || derive_path(key, kind, format)
|
|
19
21
|
|
|
20
22
|
common = {
|
|
21
23
|
raw: raw,
|
|
22
|
-
key: key, path: path,
|
|
24
|
+
key: key, path: path, lane: lane,
|
|
23
25
|
schema: raw["schema"], owner: raw["owner"],
|
|
24
26
|
format: format,
|
|
25
27
|
publish_targets: publish_targets(raw)
|
|
@@ -30,13 +32,13 @@ module Textus
|
|
|
30
32
|
klass.from_raw(common, raw)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
def self.parse_source(raw,
|
|
36
|
-
block = raw["source"]
|
|
37
|
-
|
|
35
|
+
# Parse the optional `source:` block. Returns nil when absent (workflow
|
|
36
|
+
# produced entries register their produce logic in .textus/workflows/).
|
|
37
|
+
def self.parse_source(raw, _key)
|
|
38
|
+
block = raw["source"]
|
|
39
|
+
return nil if block.nil?
|
|
38
40
|
|
|
39
|
-
Textus::
|
|
41
|
+
Textus::Manifest::Policy::Source.new(block)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# ADR 0094: `publish:` is a LIST of target objects — to-targets
|
|
@@ -52,13 +54,16 @@ module Textus
|
|
|
52
54
|
"[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
|
|
53
55
|
)
|
|
54
56
|
end
|
|
55
|
-
block.map { |t| Textus::
|
|
57
|
+
block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def self.resolve_format(raw, path)
|
|
59
61
|
declared = raw["format"]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
return declared || "markdown" if path.nil? || path.empty?
|
|
64
|
+
|
|
65
|
+
ext = File.extname(path)
|
|
66
|
+
inferred = Textus::Format.infer_from_extension(ext)
|
|
62
67
|
|
|
63
68
|
if declared.nil?
|
|
64
69
|
return inferred if inferred
|
|
@@ -66,7 +71,7 @@ module Textus
|
|
|
66
71
|
return "markdown"
|
|
67
72
|
end
|
|
68
73
|
|
|
69
|
-
raise UsageError.new("entry '#{raw["key"]}': unknown format #{declared.inspect}") unless Textus::
|
|
74
|
+
raise UsageError.new("entry '#{raw["key"]}': unknown format #{declared.inspect}") unless Textus::Format.formats.include?(declared)
|
|
70
75
|
|
|
71
76
|
if ext != "" && inferred && inferred != declared
|
|
72
77
|
raise UsageError.new(
|
|
@@ -76,6 +81,18 @@ module Textus
|
|
|
76
81
|
|
|
77
82
|
declared
|
|
78
83
|
end
|
|
84
|
+
|
|
85
|
+
# Derives the manifest-relative path from key + kind + format.
|
|
86
|
+
# Key::Path.normalize_relative_path will prepend data/ at resolution time.
|
|
87
|
+
def self.derive_path(key, kind_sym, format)
|
|
88
|
+
dir_path = key.split(".").join("/")
|
|
89
|
+
return dir_path if kind_sym == :nested
|
|
90
|
+
|
|
91
|
+
ext = Textus::Format.for(format).extensions.first
|
|
92
|
+
"#{dir_path}#{ext}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_class_method :derive_path
|
|
79
96
|
end
|
|
80
97
|
end
|
|
81
98
|
end
|
|
@@ -1,52 +1,32 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
|
-
# A produced entry
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# A produced entry — `kind: produced` with an optional `source:` block.
|
|
5
|
+
# When `source:` is present it must be `from: external` (out-of-band
|
|
6
|
+
# generator; textus detects drift but never runs it). When absent the
|
|
7
|
+
# entry is produced by a workflow file in .textus/workflows/.
|
|
7
8
|
class Produced < Base
|
|
8
|
-
attr_reader :source
|
|
9
|
+
attr_reader :source
|
|
9
10
|
|
|
10
|
-
def initialize(source:,
|
|
11
|
+
def initialize(source:, **rest)
|
|
11
12
|
super(**rest)
|
|
12
13
|
@source = source
|
|
13
|
-
@events = events || {}
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
def
|
|
18
|
-
def external? = @source.external?
|
|
19
|
-
def projection? = @source.projection?
|
|
20
|
-
def nested? = !!@raw["nested"]
|
|
21
|
-
def handler = @source.handler
|
|
22
|
-
def config = @source.config
|
|
16
|
+
def external? = @source&.external? || false
|
|
17
|
+
def nested? = !!@raw["nested"]
|
|
23
18
|
|
|
24
19
|
KIND = :produced
|
|
25
20
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# out-of-band runner — neither builds, but both still publish their
|
|
30
|
-
# existing store bytes through the same mode. A projection entry with no
|
|
31
|
-
# targets is a terminal data node: it produced data, so report :built
|
|
32
|
-
# even though nothing was emitted.
|
|
21
|
+
# Publish existing store bytes via the shared publish mode (Publish::ToPaths
|
|
22
|
+
# or Publish::None). Workflow runners handle the produce step; this method
|
|
23
|
+
# only publishes whatever bytes are already on disk.
|
|
33
24
|
def publish_via(pctx, prefix: nil)
|
|
34
|
-
|
|
35
|
-
if projection?
|
|
36
|
-
Textus::Produce::Acquire::Projection.new(container: pctx.container, call: pctx.call).run(self)
|
|
37
|
-
built = true
|
|
38
|
-
pctx.emit(:entry_produced, key: @key, envelope: pctx.reader.call(@key), sources: Array(@source.select).compact)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
emitted = publish_mode.publish(pctx, prefix: prefix)
|
|
42
|
-
return emitted if emitted
|
|
43
|
-
return nil unless built
|
|
44
|
-
|
|
45
|
-
{ kind: :built, value: { "key" => @key, "path" => Key::Path.resolve(pctx.manifest.data, self), "published_to" => [] } }
|
|
25
|
+
publish_mode.publish(pctx, prefix: prefix)
|
|
46
26
|
end
|
|
47
27
|
|
|
48
28
|
def self.from_raw(common, raw)
|
|
49
|
-
new(source: Parser.parse_source(raw, common[:key]),
|
|
29
|
+
new(source: Parser.parse_source(raw, common[:key]), **common)
|
|
50
30
|
end
|
|
51
31
|
|
|
52
32
|
Entry::REGISTRY[KIND] = self
|
|
@@ -16,6 +16,7 @@ module Textus
|
|
|
16
16
|
|
|
17
17
|
def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize
|
|
18
18
|
targets = entry.publish_targets.select(&:to_target?)
|
|
19
|
+
|
|
19
20
|
return nil if targets.empty?
|
|
20
21
|
|
|
21
22
|
data_path = pctx.manifest.resolver.resolve(entry.key).path
|
|
@@ -25,11 +26,11 @@ module Textus
|
|
|
25
26
|
|
|
26
27
|
targets.each do |t|
|
|
27
28
|
if t.renders?
|
|
28
|
-
content ||= Textus::
|
|
29
|
+
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
29
30
|
publish_bytes(render_bytes(t, content, renderer, pctx), entry.key, t, pctx, data_path, envelope)
|
|
30
31
|
elsif strip_meta?(entry)
|
|
31
|
-
content ||= Textus::
|
|
32
|
-
bytes = Textus::
|
|
32
|
+
content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
|
|
33
|
+
bytes = Textus::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
|
|
33
34
|
publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
|
|
34
35
|
else
|
|
35
36
|
# opaque / command / non-structured — publish the stored file as-is
|