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
data/lib/textus/mcp/session.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Kept for name stability (ADR 0039). The JSON schemas are DERIVED from
|
|
4
|
-
# per-verb contracts; this delegates to MCP::Catalog. The hand-written
|
|
5
|
-
# array is gone — a kwarg rename now updates the schema automatically (and
|
|
6
|
-
# the signature guard fails if the contract lags the use-case).
|
|
7
|
-
module ToolSchemas
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def all
|
|
11
|
-
Catalog.tool_schemas
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/textus/mcp.rb
DELETED
data/lib/textus/mustache.rb
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Mustache
|
|
3
|
-
MAX_DEPTH = 8
|
|
4
|
-
TAG = %r{\{\{(?<sigil>[#^/!&]?)\s*(?<name>[\w.-]+)\s*\}\}}
|
|
5
|
-
|
|
6
|
-
def self.render(template, context, strict: false, depth: 0) # rubocop:disable Metrics/AbcSize
|
|
7
|
-
raise TemplateError.new("template recursion depth #{depth} exceeded #{MAX_DEPTH}") if depth > MAX_DEPTH
|
|
8
|
-
|
|
9
|
-
out = +""
|
|
10
|
-
pos = 0
|
|
11
|
-
while (m = template.match(TAG, pos))
|
|
12
|
-
out << template[pos...m.begin(0)]
|
|
13
|
-
case m[:sigil]
|
|
14
|
-
when "!"
|
|
15
|
-
# comment, skip
|
|
16
|
-
when "#"
|
|
17
|
-
section, new_pos = parse_section(template, m, m[:name])
|
|
18
|
-
value = lookup(context, m[:name])
|
|
19
|
-
out << render_section(section, value, context, strict, depth)
|
|
20
|
-
pos = new_pos
|
|
21
|
-
next
|
|
22
|
-
when "^"
|
|
23
|
-
section, new_pos = parse_section(template, m, m[:name])
|
|
24
|
-
value = lookup(context, m[:name])
|
|
25
|
-
if falsy?(value)
|
|
26
|
-
raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH
|
|
27
|
-
|
|
28
|
-
out << render(section, context, strict: strict, depth: depth + 1)
|
|
29
|
-
end
|
|
30
|
-
pos = new_pos
|
|
31
|
-
next
|
|
32
|
-
when "/"
|
|
33
|
-
raise TemplateError.new("unexpected closing tag #{m[:name]}")
|
|
34
|
-
else
|
|
35
|
-
val = lookup(context, m[:name])
|
|
36
|
-
if val.nil?
|
|
37
|
-
raise TemplateError.new("missing variable: #{m[:name]}") if strict
|
|
38
|
-
else
|
|
39
|
-
out << val.to_s
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
pos = m.end(0)
|
|
43
|
-
end
|
|
44
|
-
out << template[pos..]
|
|
45
|
-
out
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.parse_section(template, open_match, name)
|
|
49
|
-
open_re = /\{\{#\s*#{Regexp.escape(name)}\s*\}\}|\{\{\^\s*#{Regexp.escape(name)}\s*\}\}/
|
|
50
|
-
close_re = %r{\{\{/\s*#{Regexp.escape(name)}\s*\}\}}
|
|
51
|
-
both = Regexp.union(open_re, close_re)
|
|
52
|
-
depth = 1
|
|
53
|
-
cursor = open_match.end(0)
|
|
54
|
-
while (m = template.match(both, cursor))
|
|
55
|
-
if m[0].start_with?("{{/")
|
|
56
|
-
depth -= 1
|
|
57
|
-
return [template[open_match.end(0)...m.begin(0)], m.end(0)] if depth.zero?
|
|
58
|
-
else
|
|
59
|
-
depth += 1
|
|
60
|
-
end
|
|
61
|
-
cursor = m.end(0)
|
|
62
|
-
end
|
|
63
|
-
raise TemplateError.new("unclosed section: #{name}")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.render_section(section, value, context, strict, depth)
|
|
67
|
-
raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH
|
|
68
|
-
|
|
69
|
-
case value
|
|
70
|
-
when Array
|
|
71
|
-
value.map { |v| render(section, scope_for(context, v), strict: strict, depth: depth + 1) }.join
|
|
72
|
-
when Hash
|
|
73
|
-
render(section, merge(context, value), strict: strict, depth: depth + 1)
|
|
74
|
-
when true
|
|
75
|
-
render(section, context, strict: strict, depth: depth + 1)
|
|
76
|
-
when false, nil
|
|
77
|
-
# falsy in regular section: render nothing.
|
|
78
|
-
# render_section is only called for inverted sections when falsy? is true at the call site,
|
|
79
|
-
# so this branch is only hit for normal sections with falsy values.
|
|
80
|
-
""
|
|
81
|
-
else
|
|
82
|
-
render(section, context, strict: strict, depth: depth + 1)
|
|
83
|
-
end || ""
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def self.lookup(context, name)
|
|
87
|
-
# Implicit iterator: {{.}} refers to the current scope itself (used when
|
|
88
|
-
# iterating arrays of primitive values).
|
|
89
|
-
return context["."] if name == "." && context.is_a?(Hash) && context.key?(".")
|
|
90
|
-
return context[name] if context.is_a?(Hash) && context.key?(name)
|
|
91
|
-
|
|
92
|
-
name.split(".").reduce(context) do |acc, seg|
|
|
93
|
-
return nil unless acc.is_a?(Hash)
|
|
94
|
-
|
|
95
|
-
acc[seg]
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Build the rendering scope for one iteration of a section. Hash items
|
|
100
|
-
# merge into the outer context; primitive items (strings, numbers) bind
|
|
101
|
-
# to the implicit iterator under key ".".
|
|
102
|
-
def self.scope_for(context, item)
|
|
103
|
-
return merge(context, item) if item.is_a?(Hash)
|
|
104
|
-
|
|
105
|
-
base = context.is_a?(Hash) ? context : {}
|
|
106
|
-
base.merge("." => item)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def self.merge(base, override)
|
|
110
|
-
return base unless override.is_a?(Hash)
|
|
111
|
-
|
|
112
|
-
base.merge(override)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def self.falsy?(v) = v.nil? || v == false || v == [] || v == ""
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Ports
|
|
5
|
-
# Writes an "event_error" audit row when a user hook raises during
|
|
6
|
-
# Hooks::EventBus publish. Attached at Store boot.
|
|
7
|
-
#
|
|
8
|
-
# Integration: uses Hooks::EventBus#on_error callback (chosen over a
|
|
9
|
-
# synthetic :hook_error event because the bus already owns the
|
|
10
|
-
# rescue and the failure is a bus-internal concern, not a domain
|
|
11
|
-
# event subscribers should be able to filter by key glob).
|
|
12
|
-
#
|
|
13
|
-
# Lifecycle audit rows for verb: "put" / "key_delete" / "key_mv" are written
|
|
14
|
-
# by Envelope::IO::Writer directly (it owns the
|
|
15
|
-
# audit-append-as-final-step invariant); this subscriber covers the
|
|
16
|
-
# hook-failure case the writer never sees.
|
|
17
|
-
class AuditSubscriber
|
|
18
|
-
def initialize(audit_log)
|
|
19
|
-
@audit_log = audit_log
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def attach(bus)
|
|
23
|
-
bus.on_error do |event:, hook:, key:, kwargs:, error:|
|
|
24
|
-
record_error(event: event, hook: hook, key: key, kwargs: kwargs, error: error)
|
|
25
|
-
end
|
|
26
|
-
self
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def record_error(event:, hook:, key:, kwargs:, error:)
|
|
32
|
-
extras = { "event" => event.to_s, "hook" => hook.to_s, "error" => "#{error.class}: #{error.message}" }
|
|
33
|
-
extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
|
|
34
|
-
extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
|
|
35
|
-
@audit_log.append(
|
|
36
|
-
role: Textus::Role::AUTOMATION, verb: "event_error", key: key,
|
|
37
|
-
etag_before: nil, etag_after: nil, extras: extras
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Ports
|
|
5
|
-
# ADR 0093 / job-queue model: on a canon write, enqueue a `materialize` job
|
|
6
|
-
# for each derived entry that depends on the written key (rdeps ∩ producible).
|
|
7
|
-
# Async-only — the write returns immediately; a worker (drain/serve) converges
|
|
8
|
-
# the jobs. There is no inline `sync` path and no in-process thread: freshness
|
|
9
|
-
# is re-homed to drain (at the commit/CI gate) and the daemon. A write INTO a
|
|
10
|
-
# derived entry does not fan out (recursion guard). Produce self-elevates, so
|
|
11
|
-
# the job is stamped automation. Attached at Store boot, alongside
|
|
12
|
-
# AuditSubscriber.
|
|
13
|
-
class ProduceOnWriteSubscriber
|
|
14
|
-
def initialize(container)
|
|
15
|
-
@container = container
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def attach(bus)
|
|
19
|
-
bus.on(:entry_written, :produce_on_write) do |key:, **|
|
|
20
|
-
on_write(key: key)
|
|
21
|
-
end
|
|
22
|
-
# Closes the ADR 0087 gap: a delete/rename of a source must re-materialize
|
|
23
|
-
# its orphaned dependents too, not just a write. These fire distinct
|
|
24
|
-
# events (:entry_deleted / :entry_renamed), so subscribe to each.
|
|
25
|
-
bus.on(:entry_deleted, :produce_on_delete) do |key:, **|
|
|
26
|
-
on_write(key: key)
|
|
27
|
-
end
|
|
28
|
-
bus.on(:entry_renamed, :produce_on_rename) do |from_key:, to_key:, **|
|
|
29
|
-
on_write(key: from_key)
|
|
30
|
-
on_write(key: to_key)
|
|
31
|
-
end
|
|
32
|
-
self
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def on_write(key:)
|
|
36
|
-
return if derived_write?(key) # recursion guard: produce output is not a source change
|
|
37
|
-
|
|
38
|
-
affected = Textus::Read::Rdeps.new(container: @container).call(key)["rdeps"]
|
|
39
|
-
producible = affected.select { |k| producible?(k) }
|
|
40
|
-
return if producible.empty?
|
|
41
|
-
|
|
42
|
-
queue = Textus::Ports::Queue.new(root: @container.root)
|
|
43
|
-
producible.each do |k|
|
|
44
|
-
queue.enqueue(
|
|
45
|
-
Textus::Domain::Jobs::Job.new(
|
|
46
|
-
type: "materialize", args: { "key" => k }, enqueued_by: Textus::Role::AUTOMATION,
|
|
47
|
-
),
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def derived_write?(key)
|
|
55
|
-
@container.manifest.resolver.resolve(key).entry.derived?
|
|
56
|
-
rescue Textus::Error
|
|
57
|
-
false
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# The producible scope mirrors Produce::Engine#produce_one: derived
|
|
61
|
-
# entries render+publish, and nested publish_tree entries mirror their
|
|
62
|
-
# source subtree (ADR 0047). Including the latter restores reactive
|
|
63
|
-
# re-mirroring on a write into a tree's source — dropped when the scope
|
|
64
|
-
# narrowed to `derived?` only.
|
|
65
|
-
def producible?(key)
|
|
66
|
-
entry = @container.manifest.resolver.resolve(key).entry
|
|
67
|
-
entry.derived? || !entry.publish_tree.nil?
|
|
68
|
-
rescue Textus::Error
|
|
69
|
-
false
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
require "timeout"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Produce
|
|
5
|
-
module Acquire
|
|
6
|
-
# Invokes a :resolve_handler hook handler by name under a timeout — the single
|
|
7
|
-
# home for "call the intake handler under a deadline" (ADR 0048 D1). Shared by
|
|
8
|
-
# Produce::Acquire::Intake (the internal ingest mechanism — no public verb since ADR 0079)
|
|
9
|
-
# as driven by the converge sweep (drain/serve) and `textus hook run` (ADR 0089 made
|
|
10
|
-
# ingest system-pushed; there is no read or put trigger).
|
|
11
|
-
# Always passes a Container as `caps:` so the hook contract (ADR 0027) is
|
|
12
|
-
# uniform across every entry point. Maps Timeout::Error to a UsageError;
|
|
13
|
-
# leaves any other error to the caller (call sites differ in how they wrap).
|
|
14
|
-
module Handler
|
|
15
|
-
FETCH_TIMEOUT_SECONDS = 30
|
|
16
|
-
|
|
17
|
-
module_function
|
|
18
|
-
|
|
19
|
-
def invoke(caps:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
|
|
20
|
-
Timeout.timeout(timeout) do
|
|
21
|
-
caps.rpc.invoke(:resolve_handler, handler, caps: caps, config: config, args: args)
|
|
22
|
-
end
|
|
23
|
-
rescue Timeout::Error
|
|
24
|
-
raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
require "timeout"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Produce
|
|
5
|
-
module Acquire
|
|
6
|
-
# Internal ingest executor for one machine-zone intake entry. No longer a
|
|
7
|
-
# public verb (ADR 0079 collapsed the `fetch` surface): used by the
|
|
8
|
-
# converge sweep (drain/serve) and `textus hook run` only — ingest is system-pushed
|
|
9
|
-
# (ADR 0089 removed the read-through that once also drove it).
|
|
10
|
-
class Intake
|
|
11
|
-
FETCH_TIMEOUT_SECONDS = Textus::Produce::Acquire::Handler::FETCH_TIMEOUT_SECONDS
|
|
12
|
-
|
|
13
|
-
def initialize(container:, call:)
|
|
14
|
-
@container = container
|
|
15
|
-
@call = call
|
|
16
|
-
@manifest = container.manifest
|
|
17
|
-
@schemas = container.schemas
|
|
18
|
-
@rpc = container.rpc
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# call(key) is the primary entry; run is kept as an alias for
|
|
22
|
-
# Orchestrator and FetchAll which call worker.run(key).
|
|
23
|
-
def call(key)
|
|
24
|
-
run(key)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def run(key)
|
|
28
|
-
res = @manifest.resolver.resolve(key)
|
|
29
|
-
mentry = res.entry
|
|
30
|
-
path = res.path
|
|
31
|
-
remaining = res.remaining
|
|
32
|
-
raise UsageError.new("no intake declared for '#{key}'") unless mentry.intake?
|
|
33
|
-
|
|
34
|
-
before_etag = @container.file_store.exists?(path) ? @container.file_store.etag(path) : nil
|
|
35
|
-
result = fetch_with_events(key, mentry, remaining)
|
|
36
|
-
persist_and_notify(key, mentry, result, before_etag)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def self.normalize_action_result(res, format:)
|
|
40
|
-
res = res.transform_keys(&:to_s) if res.is_a?(Hash)
|
|
41
|
-
res ||= {}
|
|
42
|
-
meta_val = res["_meta"]
|
|
43
|
-
body = res["body"]
|
|
44
|
-
content = res["content"]
|
|
45
|
-
|
|
46
|
-
case format
|
|
47
|
-
when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
|
|
48
|
-
when "text" then { meta: {}, body: body.to_s, content: nil }
|
|
49
|
-
when "json", "yaml"
|
|
50
|
-
if !content.nil?
|
|
51
|
-
{ meta: meta_val || {}, body: nil, content: content }
|
|
52
|
-
elsif !body.nil?
|
|
53
|
-
{ meta: {}, body: body.to_s, content: nil }
|
|
54
|
-
else
|
|
55
|
-
raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
|
|
56
|
-
end
|
|
57
|
-
else
|
|
58
|
-
raise Textus::UsageError.new("unknown format #{format.inspect}")
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def fetch_events
|
|
65
|
-
@fetch_events ||= Textus::Produce::Events.from(container: @container, call: @call)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# ADR 0079: a per-rule fetch_timeout_seconds override was an accepted loss
|
|
69
|
-
# in the fetch:/retention: → lifecycle: collapse; the constant ceiling
|
|
70
|
-
# applies to every intake.
|
|
71
|
-
def fetch_timeout_for(_key)
|
|
72
|
-
FETCH_TIMEOUT_SECONDS
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def fetch_with_events(key, mentry, remaining)
|
|
76
|
-
fetch_events.started(key)
|
|
77
|
-
call_intake(key, mentry, remaining)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def call_intake(key, mentry, remaining)
|
|
81
|
-
Textus::Produce::Acquire::Handler.invoke(
|
|
82
|
-
caps: @container, handler: mentry.handler,
|
|
83
|
-
config: mentry.config,
|
|
84
|
-
args: { trigger_key: key, leaf_segments: remaining || [] },
|
|
85
|
-
label: "intake", timeout: fetch_timeout_for(key)
|
|
86
|
-
)
|
|
87
|
-
rescue Textus::Error => e
|
|
88
|
-
fetch_events.failed(key, e)
|
|
89
|
-
raise
|
|
90
|
-
rescue StandardError => e
|
|
91
|
-
fetch_events.failed(key, e)
|
|
92
|
-
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def persist_and_notify(key, mentry, result, before_etag)
|
|
96
|
-
normalized = self.class.normalize_action_result(result, format: mentry.format)
|
|
97
|
-
Textus::Domain::Policy::GuardFactory.new(
|
|
98
|
-
manifest: @manifest, schemas: @schemas,
|
|
99
|
-
).for(:converge, key).check!(
|
|
100
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
101
|
-
actor: @call.role, transition: :converge, origin: nil,
|
|
102
|
-
target: key, envelope: nil, manifest: @manifest
|
|
103
|
-
),
|
|
104
|
-
)
|
|
105
|
-
envelope = writer.put(
|
|
106
|
-
key,
|
|
107
|
-
mentry: mentry,
|
|
108
|
-
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
109
|
-
meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
|
|
110
|
-
),
|
|
111
|
-
)
|
|
112
|
-
change = detect_change(before_etag, envelope)
|
|
113
|
-
fetch_events.fetched(key, envelope, change)
|
|
114
|
-
envelope
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def detect_change(before_etag, envelope)
|
|
118
|
-
if before_etag.nil? then :created
|
|
119
|
-
elsif envelope.etag == before_etag then :unchanged
|
|
120
|
-
else :updated
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def writer
|
|
125
|
-
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Produce
|
|
5
|
-
module Acquire
|
|
6
|
-
# Builds an entry's DATA artifact (ADR 0094) by running the projection
|
|
7
|
-
# pipeline; rendering is a publish concern. External entries are NOT built
|
|
8
|
-
# here — they are generated by an out-of-band runner; Derived#publish_via
|
|
9
|
-
# filters them out before reaching this point.
|
|
10
|
-
#
|
|
11
|
-
# Merges the former Write::DataBuilder wrapper and Builder::Pipeline module
|
|
12
|
-
# into one class (ADR 0100 produce/ topology refactor).
|
|
13
|
-
class Projection
|
|
14
|
-
# Injects provenance metadata as the first key in the serialized output.
|
|
15
|
-
# Carries only deterministic provenance (`from`/`reduce`) — the volatile
|
|
16
|
-
# `generated_at` is deliberately NOT stamped, so the built artifact is
|
|
17
|
-
# content-addressed and a rebuild is a byte-for-byte no-op (ADR 0070).
|
|
18
|
-
# Build time lives out of the tracked artifact.
|
|
19
|
-
module InjectMeta
|
|
20
|
-
def self.call(content_hash, mentry)
|
|
21
|
-
meta = {}
|
|
22
|
-
if mentry.derived?
|
|
23
|
-
src = mentry.source
|
|
24
|
-
if src.projection?
|
|
25
|
-
from = Array(src.select).compact
|
|
26
|
-
meta["from"] = from unless from.empty?
|
|
27
|
-
meta["reduce"] = src.transform if src.transform
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
out = { "_meta" => meta }
|
|
32
|
-
content_hash.each { |k, v| out[k] = v unless k == "_meta" }
|
|
33
|
-
out
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
Deps = Data.define(:manifest, :reader, :lister, :rpc, :transform_context)
|
|
38
|
-
|
|
39
|
-
def self.renderers
|
|
40
|
-
@renderers ||= {
|
|
41
|
-
"text" => Produce::Acquire::Serializer::Text,
|
|
42
|
-
"json" => Produce::Acquire::Serializer::Json,
|
|
43
|
-
"yaml" => Produce::Acquire::Serializer::Yaml,
|
|
44
|
-
}
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def initialize(container:, call:)
|
|
48
|
-
@container = container
|
|
49
|
-
@call = call
|
|
50
|
-
@manifest = container.manifest
|
|
51
|
-
@file_store = container.file_store
|
|
52
|
-
@rpc = container.rpc
|
|
53
|
-
@root = container.root
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Runs the projection pipeline for `mentry` and returns the on-disk
|
|
57
|
-
# target_path string.
|
|
58
|
-
def run(mentry)
|
|
59
|
-
reader = Textus::Read::Get.new(container: @container, call: @call)
|
|
60
|
-
# Projections must be able to read source data from any nested entry,
|
|
61
|
-
# including keyless (publish_tree) ones like knowledge.decisions.
|
|
62
|
-
# The `include_keyless: true` option makes the resolver walk those dirs
|
|
63
|
-
# without exposing them on the public `list` / CLI surface (ADR 0047).
|
|
64
|
-
resolver = @manifest.resolver
|
|
65
|
-
lister = lambda do |prefix:|
|
|
66
|
-
resolver.enumerate(prefix: prefix, include_keyless: true)
|
|
67
|
-
.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
|
|
68
|
-
end
|
|
69
|
-
self.class.pipeline_run(
|
|
70
|
-
mentry: mentry,
|
|
71
|
-
deps: Deps.new(
|
|
72
|
-
manifest: @manifest,
|
|
73
|
-
reader: reader.method(:call),
|
|
74
|
-
lister: lister,
|
|
75
|
-
rpc: @rpc,
|
|
76
|
-
transform_context: @container,
|
|
77
|
-
),
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def self.pipeline_run(mentry:, deps:)
|
|
82
|
-
# 1. Load sources + project + reduce. Only projection-derived entries are
|
|
83
|
-
# buildable in-process; External entries are generated out-of-band and are
|
|
84
|
-
# filtered out upstream (Derived#publish_via), so reaching here with a
|
|
85
|
-
# non-projection source is a wiring bug — fail loudly rather than emit an
|
|
86
|
-
# empty payload (and never re-stamp the volatile generated_at, ADR 0070).
|
|
87
|
-
unless mentry.projection?
|
|
88
|
-
raise UsageError.new(
|
|
89
|
-
"builder: '#{mentry.key}' is not a projection-derived entry; only projections are buildable",
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
data =
|
|
94
|
-
Textus::Projection.new(
|
|
95
|
-
reader: deps.reader,
|
|
96
|
-
spec: mentry.source.projection_spec,
|
|
97
|
-
lister: deps.lister,
|
|
98
|
-
rpc: deps.rpc,
|
|
99
|
-
transform_context: deps.transform_context,
|
|
100
|
-
).run
|
|
101
|
-
|
|
102
|
-
# 2. Serialize as DATA. Rendering through a template is a publish concern
|
|
103
|
-
# (ADR 0094) — the build never consults a template.
|
|
104
|
-
klass = renderers[mentry.format] or
|
|
105
|
-
raise UsageError.new("builder: unsupported data format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
106
|
-
bytes = klass.new.call(mentry: mentry, data: data)
|
|
107
|
-
|
|
108
|
-
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
109
|
-
target_path = Key::Path.resolve(deps.manifest.data, mentry)
|
|
110
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
111
|
-
write_if_changed(target_path, bytes, mentry.format)
|
|
112
|
-
|
|
113
|
-
target_path
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Built artifacts are content-addressed (no volatile timestamp, ADR 0070),
|
|
117
|
-
# so identity is plain byte-equality: skip the write when nothing changed.
|
|
118
|
-
# `format` is retained for signature stability across renderers.
|
|
119
|
-
def self.write_if_changed(target_path, bytes, _format)
|
|
120
|
-
return if File.exist?(target_path) && File.binread(target_path) == bytes
|
|
121
|
-
|
|
122
|
-
File.binwrite(target_path, bytes)
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Produce
|
|
5
|
-
module Acquire
|
|
6
|
-
class Serializer
|
|
7
|
-
class Json < Serializer
|
|
8
|
-
def call(mentry:, data:)
|
|
9
|
-
content = default_shape(mentry, data)
|
|
10
|
-
final = Produce::Acquire::Projection::InjectMeta.call(content, mentry)
|
|
11
|
-
Entry.for_format("json").serialize(meta: {}, body: "", content: final)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def default_shape(mentry, data)
|
|
17
|
-
has_transform = mentry.projection? &&
|
|
18
|
-
mentry.source.transform
|
|
19
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
20
|
-
data
|
|
21
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
22
|
-
{ "entries" => data["entries"] }
|
|
23
|
-
else
|
|
24
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
module Acquire
|
|
4
|
-
class Serializer
|
|
5
|
-
class Text < Serializer
|
|
6
|
-
def call(mentry:, data:) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
|
-
# Text format serializes data as plain-text. Rendering through a
|
|
8
|
-
# template is a publish concern (ADR 0094) — build emits data only.
|
|
9
|
-
body = data.is_a?(Hash) ? data.to_s : data.inspect
|
|
10
|
-
Entry.for_format("text").serialize(meta: {}, body: body)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Produce
|
|
5
|
-
module Acquire
|
|
6
|
-
class Serializer
|
|
7
|
-
class Yaml < Serializer
|
|
8
|
-
def call(mentry:, data:)
|
|
9
|
-
content = default_shape(mentry, data)
|
|
10
|
-
final = Produce::Acquire::Projection::InjectMeta.call(content, mentry)
|
|
11
|
-
Entry.for_format("yaml").serialize(meta: {}, body: "", content: final)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def default_shape(mentry, data)
|
|
17
|
-
has_transform = mentry.projection? &&
|
|
18
|
-
mentry.source.transform
|
|
19
|
-
if has_transform && data.is_a?(Hash) && !data.key?("entries")
|
|
20
|
-
data
|
|
21
|
-
elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
|
|
22
|
-
{ "entries" => data["entries"] }
|
|
23
|
-
else
|
|
24
|
-
data.is_a?(Hash) ? data : { "entries" => Array(data) }
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
module Acquire
|
|
4
|
-
# Abstract base for output serializers. Each concrete serializer owns
|
|
5
|
-
# producing the bytes for one manifest format (json/yaml/text).
|
|
6
|
-
# Rendering through a template is a publish concern (ADR 0094) — serializers
|
|
7
|
-
# here only serialize data; they take no arguments.
|
|
8
|
-
class Serializer
|
|
9
|
-
def call(mentry:, data:)
|
|
10
|
-
_ = mentry
|
|
11
|
-
_ = data
|
|
12
|
-
raise NotImplementedError.new("#{self.class.name}#call not implemented")
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|