textus 0.53.0 → 0.54.1
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 +52 -0
- data/README.md +53 -73
- data/SPEC.md +2 -5
- data/docs/architecture/README.md +149 -159
- data/lib/textus/action/accept.rb +0 -9
- data/lib/textus/action/audit.rb +0 -2
- data/lib/textus/action/blame.rb +0 -2
- data/lib/textus/action/boot.rb +2 -11
- data/lib/textus/action/data_mv.rb +1 -3
- data/lib/textus/action/deps.rb +1 -15
- data/lib/textus/action/doctor.rb +0 -2
- data/lib/textus/action/drain.rb +4 -6
- data/lib/textus/action/enqueue.rb +3 -5
- data/lib/textus/action/get.rb +1 -3
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +1 -3
- data/lib/textus/action/key_delete.rb +0 -10
- data/lib/textus/action/key_delete_prefix.rb +1 -3
- data/lib/textus/action/key_mv.rb +1 -15
- data/lib/textus/action/key_mv_prefix.rb +1 -3
- data/lib/textus/action/list.rb +0 -2
- data/lib/textus/action/propose.rb +0 -2
- data/lib/textus/action/published.rb +0 -2
- data/lib/textus/action/pulse.rb +7 -38
- data/lib/textus/action/put.rb +1 -12
- data/lib/textus/action/rdeps.rb +2 -12
- data/lib/textus/action/reject.rb +0 -12
- data/lib/textus/action/rule_explain.rb +0 -4
- data/lib/textus/action/rule_lint.rb +1 -3
- data/lib/textus/action/rule_list.rb +0 -4
- data/lib/textus/action/schema_envelope.rb +0 -2
- data/lib/textus/action/uid.rb +0 -2
- data/lib/textus/action/where.rb +0 -2
- data/lib/textus/action/write_verb.rb +4 -18
- data/lib/textus/boot.rb +40 -109
- data/lib/textus/command.rb +6 -1
- data/lib/textus/container.rb +21 -5
- data/lib/textus/core/freshness/evaluator.rb +18 -34
- data/lib/textus/core/freshness/verdict.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/protocol_version.rb +6 -6
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +54 -0
- data/lib/textus/doctor/check.rb +0 -1
- data/lib/textus/doctor.rb +3 -42
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +19 -6
- data/lib/textus/errors.rb +9 -5
- 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 +58 -12
- data/lib/textus/gate.rb +2 -0
- data/lib/textus/init.rb +16 -79
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/{background → jobs}/plan.rb +1 -1
- 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/{background → jobs}/worker.rb +2 -2
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +4 -19
- data/lib/textus/manifest/entry/parser.rb +27 -10
- data/lib/textus/manifest/entry/produced.rb +13 -34
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -4
- 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/manifest/policy/source.rb +14 -62
- data/lib/textus/manifest/policy.rb +2 -5
- data/lib/textus/manifest/resolver.rb +2 -2
- data/lib/textus/manifest/schema/contract.rb +61 -0
- data/lib/textus/manifest/schema/keys.rb +6 -20
- data/lib/textus/manifest/schema/semantics.rb +232 -0
- data/lib/textus/manifest/schema/validator.rb +24 -302
- data/lib/textus/manifest/schema/vocabulary.rb +1 -0
- data/lib/textus/manifest/schema.rb +1 -4
- data/lib/textus/manifest.rb +1 -3
- data/lib/textus/ports/{queue → job_store}/job.rb +1 -1
- data/lib/textus/ports/{queue.rb → job_store.rb} +6 -13
- data/lib/textus/produce/engine.rb +50 -0
- data/lib/textus/produce/render.rb +20 -0
- data/lib/textus/session.rb +15 -8
- data/lib/textus/store.rb +5 -14
- data/lib/textus/surfaces/mcp/catalog.rb +23 -11
- data/lib/textus/surfaces/mcp/errors.rb +0 -9
- data/lib/textus/surfaces/mcp/server.rb +87 -110
- data/lib/textus/surfaces/role_scope.rb +0 -4
- data/lib/textus/surfaces/watcher.rb +3 -3
- data/lib/textus/types.rb +15 -0
- data/lib/textus/version.rb +2 -2
- 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 +99 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +12 -1
- metadata +67 -66
- data/lib/textus/action/pulse/scanner.rb +0 -118
- data/lib/textus/background/job/base.rb +0 -27
- data/lib/textus/background/job/materialize.rb +0 -31
- data/lib/textus/background/job/refresh.rb +0 -22
- data/lib/textus/background/job/sweep.rb +0 -31
- data/lib/textus/background/job.rb +0 -19
- data/lib/textus/background/planner/plan.rb +0 -113
- data/lib/textus/background/retention/apply.rb +0 -50
- data/lib/textus/doctor/check/handler_permit.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -25
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -211
- data/lib/textus/events.rb +0 -21
- data/lib/textus/init/templates/machine_intake.rb +0 -49
- data/lib/textus/init/templates/orientation_reducer.rb +0 -21
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/manifest/policy/handler_permit.rb +0 -17
- data/lib/textus/pipeline/acquire/handler.rb +0 -29
- data/lib/textus/pipeline/acquire/intake.rb +0 -132
- data/lib/textus/pipeline/acquire/projection.rb +0 -129
- data/lib/textus/pipeline/acquire/serializer/json.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer/text.rb +0 -16
- data/lib/textus/pipeline/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/pipeline/acquire/serializer.rb +0 -17
- data/lib/textus/pipeline/engine.rb +0 -97
- data/lib/textus/pipeline/render.rb +0 -25
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/step/base.rb +0 -35
- data/lib/textus/step/builtin/csv_fetch.rb +0 -19
- data/lib/textus/step/builtin/ical_events_fetch.rb +0 -30
- data/lib/textus/step/builtin/json_fetch.rb +0 -18
- data/lib/textus/step/builtin/markdown_links_fetch.rb +0 -20
- data/lib/textus/step/builtin/rss_fetch.rb +0 -26
- data/lib/textus/step/builtin.rb +0 -22
- data/lib/textus/step/catalog.rb +0 -37
- data/lib/textus/step/context.rb +0 -74
- data/lib/textus/step/discovery.rb +0 -24
- data/lib/textus/step/error_log.rb +0 -32
- data/lib/textus/step/event_bus.rb +0 -113
- data/lib/textus/step/fetch.rb +0 -13
- data/lib/textus/step/fire_report.rb +0 -23
- data/lib/textus/step/loader.rb +0 -108
- data/lib/textus/step/observe.rb +0 -31
- data/lib/textus/step/registry_store.rb +0 -66
- data/lib/textus/step/signature.rb +0 -31
- data/lib/textus/step/transform.rb +0 -12
- data/lib/textus/step/validate.rb +0 -11
- data/lib/textus/step.rb +0 -10
- data/lib/textus/surfaces/mcp/session.rb +0 -9
- data/lib/textus/surfaces/mcp/tool_schemas.rb +0 -17
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Pipeline
|
|
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, :steps, :transform_context)
|
|
38
|
-
|
|
39
|
-
def self.renderers
|
|
40
|
-
@renderers ||= {
|
|
41
|
-
"text" => Textus::Pipeline::Acquire::Serializer::Text,
|
|
42
|
-
"json" => Textus::Pipeline::Acquire::Serializer::Json,
|
|
43
|
-
"yaml" => Textus::Pipeline::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
|
-
@steps = container.steps
|
|
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 = lambda do |key|
|
|
60
|
-
Textus::Action::Get.new(key: key).call(container: @container, call: @call)
|
|
61
|
-
end
|
|
62
|
-
# Projections must be able to read source data from any nested entry,
|
|
63
|
-
# including keyless (publish_tree) ones like knowledge.decisions.
|
|
64
|
-
# The `include_keyless: true` option makes the resolver walk those dirs
|
|
65
|
-
# without exposing them on the public `list` / CLI surface (ADR 0047).
|
|
66
|
-
resolver = @manifest.resolver
|
|
67
|
-
lister = lambda do |prefix:|
|
|
68
|
-
resolver.enumerate(prefix: prefix, include_keyless: true)
|
|
69
|
-
.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } }
|
|
70
|
-
end
|
|
71
|
-
self.class.pipeline_run(
|
|
72
|
-
mentry: mentry,
|
|
73
|
-
deps: Deps.new(
|
|
74
|
-
manifest: @manifest,
|
|
75
|
-
reader: reader,
|
|
76
|
-
lister: lister,
|
|
77
|
-
steps: @steps,
|
|
78
|
-
transform_context: @container,
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.pipeline_run(mentry:, deps:)
|
|
84
|
-
# 1. Load sources + project + reduce. Only projection-derived entries are
|
|
85
|
-
# buildable in-process; External entries are generated out-of-band and are
|
|
86
|
-
# filtered out upstream (Derived#publish_via), so reaching here with a
|
|
87
|
-
# non-projection source is a wiring bug — fail loudly rather than emit an
|
|
88
|
-
# empty payload (and never re-stamp the volatile generated_at, ADR 0070).
|
|
89
|
-
unless mentry.projection?
|
|
90
|
-
raise UsageError.new(
|
|
91
|
-
"builder: '#{mentry.key}' is not a projection-derived entry; only projections are buildable",
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
data =
|
|
96
|
-
Textus::Projection.new(
|
|
97
|
-
reader: deps.reader,
|
|
98
|
-
spec: mentry.source.projection_spec,
|
|
99
|
-
lister: deps.lister,
|
|
100
|
-
steps: deps.steps,
|
|
101
|
-
transform_context: deps.transform_context,
|
|
102
|
-
).run
|
|
103
|
-
|
|
104
|
-
# 2. Serialize as DATA. Rendering through a template is a publish concern
|
|
105
|
-
# (ADR 0094) — the build never consults a template.
|
|
106
|
-
klass = renderers[mentry.format] or
|
|
107
|
-
raise UsageError.new("builder: unsupported data format #{mentry.format.inspect} for '#{mentry.key}'")
|
|
108
|
-
bytes = klass.new.call(mentry: mentry, data: data)
|
|
109
|
-
|
|
110
|
-
# 3. Write (idempotent: skip if only generated_at would differ)
|
|
111
|
-
target_path = Key::Path.resolve(deps.manifest.data, mentry)
|
|
112
|
-
FileUtils.mkdir_p(File.dirname(target_path))
|
|
113
|
-
write_if_changed(target_path, bytes, mentry.format)
|
|
114
|
-
|
|
115
|
-
target_path
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Built artifacts are content-addressed (no volatile timestamp, ADR 0070),
|
|
119
|
-
# so identity is plain byte-equality: skip the write when nothing changed.
|
|
120
|
-
# `format` is retained for signature stability across renderers.
|
|
121
|
-
def self.write_if_changed(target_path, bytes, _format)
|
|
122
|
-
return if File.exist?(target_path) && File.binread(target_path) == bytes
|
|
123
|
-
|
|
124
|
-
File.binwrite(target_path, bytes)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Pipeline
|
|
5
|
-
module Acquire
|
|
6
|
-
class Serializer
|
|
7
|
-
class Json < Serializer
|
|
8
|
-
def call(mentry:, data:)
|
|
9
|
-
content = default_shape(mentry, data)
|
|
10
|
-
final = Textus::Pipeline::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 Pipeline
|
|
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 Pipeline
|
|
5
|
-
module Acquire
|
|
6
|
-
class Serializer
|
|
7
|
-
class Yaml < Serializer
|
|
8
|
-
def call(mentry:, data:)
|
|
9
|
-
content = default_shape(mentry, data)
|
|
10
|
-
final = Textus::Pipeline::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 Pipeline
|
|
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
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Pipeline
|
|
3
|
-
# The single convergence engine (ADR 0093/0094). "Make these machine entries
|
|
4
|
-
# current from upstream." Acquire is per-`from`; publish is one uniform
|
|
5
|
-
# `publish_via` entry point for all kinds (ADR 0094):
|
|
6
|
-
# intake (from: handler) -> re-pull (Produce::Acquire::Intake), then publish_via
|
|
7
|
-
# derived (from: project) -> build data + publish_via (ToPaths or None)
|
|
8
|
-
# derived (from: command) -> skip the build; publish_via publishes
|
|
9
|
-
# existing store bytes via mode resolution
|
|
10
|
-
# (None when no targets -> skipped)
|
|
11
|
-
# Runs as the converge build actor (self-elevating); the passed `call`
|
|
12
|
-
# supplies only correlation_id/dry_run. Callers choose the key set: the
|
|
13
|
-
# write subscriber passes rdeps ∩ derived; the converge pass passes
|
|
14
|
-
# all-derived + stale-intake.
|
|
15
|
-
class Engine
|
|
16
|
-
# Locked + failure-isolated convergence — the entry point worker handlers
|
|
17
|
-
# call to materialize a key set (ADR 0093 / job-queue model). A held lock
|
|
18
|
-
# is a soft miss (an in-flight build/converge already produces fresh
|
|
19
|
-
# output); any other error is republished as :produce_failed and never
|
|
20
|
-
# raised at the caller (ADR 0087 §5 failure isolation, preserved).
|
|
21
|
-
def self.converge(container:, call:, keys:)
|
|
22
|
-
Textus::Ports::BuildLock.with(root: container.root) do
|
|
23
|
-
new(container: container, call: call).call(keys: keys)
|
|
24
|
-
end
|
|
25
|
-
rescue Textus::BuildInProgress
|
|
26
|
-
nil
|
|
27
|
-
rescue Textus::Error => e
|
|
28
|
-
container.steps.publish(
|
|
29
|
-
:produce_failed,
|
|
30
|
-
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
31
|
-
keys: keys, error: e.message
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def initialize(container:, call:)
|
|
36
|
-
@container = container
|
|
37
|
-
@call = call
|
|
38
|
-
@manifest = container.manifest
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# keys: the machine entry keys to converge. Returns
|
|
42
|
-
# { produced: [k...], skipped: [k...], failed: [{ "key"=>, "error"=> }...] }
|
|
43
|
-
def call(keys:)
|
|
44
|
-
build_call = build_actor_call
|
|
45
|
-
context = build_context(build_call)
|
|
46
|
-
out = { produced: [], skipped: [], failed: [] }
|
|
47
|
-
|
|
48
|
-
keys.each do |key|
|
|
49
|
-
produce_one(key, build_call, context, out)
|
|
50
|
-
rescue Textus::Error => e
|
|
51
|
-
out[:failed] << { "key" => key, "error" => e.message }
|
|
52
|
-
end
|
|
53
|
-
out
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# Acquire is per-`from`; publish is one uniform entry point (publish_via)
|
|
59
|
-
# for every kind. The command emit-vs-skip falls out of publish-mode
|
|
60
|
-
# resolution (Publish::None when no targets), so there is no command branch.
|
|
61
|
-
def produce_one(key, build_call, context, out)
|
|
62
|
-
entry = @manifest.resolver.resolve(key).entry
|
|
63
|
-
|
|
64
|
-
if entry.intake?
|
|
65
|
-
Textus::Pipeline::Acquire::Intake.new(container: @container, call: build_call).run(key) # acquire: re-pull
|
|
66
|
-
entry.publish_via(context) # emit any targets
|
|
67
|
-
out[:produced] << key # a fetch is production
|
|
68
|
-
else
|
|
69
|
-
result = entry.publish_via(context) # derived builds inside; command publishes-or-None
|
|
70
|
-
result.nil? ? (out[:skipped] << key) : (out[:produced] << key)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def build_actor_call
|
|
75
|
-
build_role = @manifest.policy.actor_for("converge") or
|
|
76
|
-
raise Textus::UsageError.new(
|
|
77
|
-
"no role holds the 'converge' capability",
|
|
78
|
-
hint: "declare a role with `can: [converge]` in .textus/manifest.yaml",
|
|
79
|
-
)
|
|
80
|
-
Textus::Call.build(
|
|
81
|
-
role: build_role,
|
|
82
|
-
correlation_id: @call.correlation_id,
|
|
83
|
-
dry_run: @call.dry_run,
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def build_context(call)
|
|
88
|
-
Textus::Manifest::Entry::Base::PublishContext.new(
|
|
89
|
-
container: @container, call: call,
|
|
90
|
-
reader: lambda { |key|
|
|
91
|
-
Textus::Action::Get.new(key: key).call(container: @container, call: call)
|
|
92
|
-
}
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
require "mustache"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Pipeline
|
|
5
|
-
# Renders an entry's stored DATA into the bytes for one publish target
|
|
6
|
-
# (ADR 0094). Relocates the Mustache logic that used to live in the
|
|
7
|
-
# build-time Markdown renderer. Provenance is NOT added here — it lives in
|
|
8
|
-
# the data's `_meta`; a template surfaces it if the output should show it.
|
|
9
|
-
# A verbatim target (no template) is the caller's job to copy.
|
|
10
|
-
class Render
|
|
11
|
-
def initialize(template_loader:)
|
|
12
|
-
@template_loader = template_loader
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# target: a rendering Policy::PublishTarget. data: parsed entry data.
|
|
16
|
-
# boot: boot context hash or nil. Returns the rendered String.
|
|
17
|
-
def bytes_for(target:, data:, boot:)
|
|
18
|
-
raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
|
|
19
|
-
|
|
20
|
-
ctx = target.inject_boot ? data.merge("boot" => boot) : data
|
|
21
|
-
Mustache.render(@template_loader.call(target.template), ctx)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
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
|
-
# Step::EventBus publish. Attached at Store boot.
|
|
7
|
-
#
|
|
8
|
-
# Integration: uses Step::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(registry)
|
|
23
|
-
registry.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
|
data/lib/textus/projection.rb
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
require "timeout"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
class Projection
|
|
6
|
-
MAX_LIMIT = 1000
|
|
7
|
-
REDUCER_TIMEOUT_SECONDS = 2
|
|
8
|
-
|
|
9
|
-
# `reader` — a callable `->(key) { envelope_or_nil }`. `Read::Get` is a pure
|
|
10
|
-
# read on every path (ADR 0089): it annotates freshness but never ingests,
|
|
11
|
-
# so materialization and any other reader share the same side-effect-free read.
|
|
12
|
-
# `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
|
|
13
|
-
# `rpc` — a `Step::RpcRegistry` used to dispatch `transform_rows` callables.
|
|
14
|
-
# `transform_context` — capability object handed to transform reducers as `caps:`.
|
|
15
|
-
def initialize(reader:, spec:, lister:, steps:, transform_context:)
|
|
16
|
-
@reader = reader
|
|
17
|
-
@spec = spec || {}
|
|
18
|
-
@lister = lister
|
|
19
|
-
@steps = steps
|
|
20
|
-
@transform_context = transform_context
|
|
21
|
-
@limit = (@spec["limit"] || MAX_LIMIT).to_i
|
|
22
|
-
raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def run
|
|
26
|
-
keys = collect_keys
|
|
27
|
-
explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
|
|
28
|
-
pluck_key = explicit_pluck && Array(@spec["pluck"]).include?("_key")
|
|
29
|
-
rows = keys.map do |key|
|
|
30
|
-
env = @reader.call(key)
|
|
31
|
-
row = pluck(env.meta, env.body)
|
|
32
|
-
if explicit_pluck
|
|
33
|
-
pluck_key ? row.merge("_key" => key) : row
|
|
34
|
-
else
|
|
35
|
-
row.merge("_key" => key)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
reduced = apply_reducer(rows)
|
|
39
|
-
# Reducers may return either an Array of rows (legacy / templated builds)
|
|
40
|
-
# or a Hash that becomes the structured-format payload base. In the Hash
|
|
41
|
-
# case, downstream sort/limit/position markers don't apply.
|
|
42
|
-
return reduced if reduced.is_a?(Hash)
|
|
43
|
-
|
|
44
|
-
rows = reduced
|
|
45
|
-
rows = sort(rows)
|
|
46
|
-
rows = rows.first(@limit)
|
|
47
|
-
mark_positions(rows)
|
|
48
|
-
# No `generated_at` in the payload — the built artifact is content-addressed
|
|
49
|
-
# (ADR 0070); volatile build time is kept out of the tracked output.
|
|
50
|
-
{ "entries" => rows, "count" => rows.length }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def apply_reducer(rows)
|
|
56
|
-
name = @spec["transform"] or return rows
|
|
57
|
-
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
58
|
-
@steps.invoke(:transform, name,
|
|
59
|
-
caps: @transform_context,
|
|
60
|
-
rows: rows,
|
|
61
|
-
config: @spec["transform_config"] || {})
|
|
62
|
-
end
|
|
63
|
-
rescue Timeout::Error
|
|
64
|
-
raise UsageError.new("transform '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def collect_keys
|
|
68
|
-
prefixes = Array(@spec["select"])
|
|
69
|
-
prefixes.flat_map { |p| @lister.call(prefix: p).map { |row| row["key"] } }.uniq
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def pluck(frontmatter, body)
|
|
73
|
-
fields = @spec["pluck"]
|
|
74
|
-
if fields.nil? || fields == "*"
|
|
75
|
-
frontmatter
|
|
76
|
-
else
|
|
77
|
-
Array(fields).each_with_object({}) do |f, h|
|
|
78
|
-
if f == "body"
|
|
79
|
-
h["body"] = body
|
|
80
|
-
elsif frontmatter.key?(f)
|
|
81
|
-
h[f] = frontmatter[f]
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Adds `_first`, `_last`, and `_index` markers so templates can emit
|
|
88
|
-
# delimiters (e.g. JSON commas) via {{^_last}},{{/_last}}.
|
|
89
|
-
def mark_positions(rows)
|
|
90
|
-
last_idx = rows.length - 1
|
|
91
|
-
rows.each_with_index do |row, i|
|
|
92
|
-
row["_index"] = i
|
|
93
|
-
row["_first"] = i.zero?
|
|
94
|
-
row["_last"] = (i == last_idx)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def sort(rows)
|
|
99
|
-
sb = @spec["sort_by"] or return rows
|
|
100
|
-
rows.sort_by { |r| r[sb].to_s }
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
data/lib/textus/step/base.rb
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Step
|
|
5
|
-
class Base
|
|
6
|
-
class << self
|
|
7
|
-
# The step kind is derived from class hierarchy.
|
|
8
|
-
def kind
|
|
9
|
-
return :fetch if defined?(Step::Fetch) && self <= Step::Fetch
|
|
10
|
-
return :transform if defined?(Step::Transform) && self <= Step::Transform
|
|
11
|
-
return :validate if defined?(Step::Validate) && self <= Step::Validate
|
|
12
|
-
return :observe if defined?(Step::Observe) && self <= Step::Observe
|
|
13
|
-
|
|
14
|
-
raise NotImplementedError.new("#{self} is not a known step kind")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Required #call kwargs the loader validates against the subclass.
|
|
18
|
-
def required_kwargs = []
|
|
19
|
-
|
|
20
|
-
# Built-ins (and only built-ins) override the registered name when the
|
|
21
|
-
# Ruby class name can't carry it (e.g. "markdown-links").
|
|
22
|
-
def step_name(value = :__read__)
|
|
23
|
-
if value == :__read__
|
|
24
|
-
@step_name
|
|
25
|
-
else
|
|
26
|
-
@step_name = value.to_s
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Assigned by the loader/registry at registration time.
|
|
32
|
-
attr_accessor :name
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "csv"
|
|
4
|
-
require "yaml"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Step
|
|
8
|
-
module Builtin
|
|
9
|
-
class CsvFetch < Step::Fetch
|
|
10
|
-
step_name "csv"
|
|
11
|
-
def call(config:, args:, **)
|
|
12
|
-
_ = args
|
|
13
|
-
rows = CSV.parse(config["bytes"].to_s, headers: true).map(&:to_h)
|
|
14
|
-
{ _meta: {}, body: YAML.dump(rows) }
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Step
|
|
7
|
-
module Builtin
|
|
8
|
-
class IcalEventsFetch < Step::Fetch
|
|
9
|
-
step_name "ical-events"
|
|
10
|
-
def call(config:, args:, **)
|
|
11
|
-
_ = args
|
|
12
|
-
events_list = []
|
|
13
|
-
current = nil
|
|
14
|
-
config["bytes"].to_s.each_line do |line|
|
|
15
|
-
line = line.strip
|
|
16
|
-
case line
|
|
17
|
-
when "BEGIN:VEVENT" then current = {}
|
|
18
|
-
when "END:VEVENT"
|
|
19
|
-
events_list << current if current
|
|
20
|
-
current = nil
|
|
21
|
-
when /\A(SUMMARY|DTSTART|DTEND|UID|LOCATION|DESCRIPTION):(.*)\z/
|
|
22
|
-
current[Regexp.last_match(1).downcase] = Regexp.last_match(2) if current
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
{ _meta: {}, body: YAML.dump(events_list) }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "yaml"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Step
|
|
8
|
-
module Builtin
|
|
9
|
-
class JsonFetch < Step::Fetch
|
|
10
|
-
step_name "json"
|
|
11
|
-
def call(config:, args:, **)
|
|
12
|
-
_ = args
|
|
13
|
-
{ _meta: {}, body: YAML.dump(JSON.parse(config["bytes"].to_s)) }
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Step
|
|
7
|
-
module Builtin
|
|
8
|
-
class MarkdownLinksFetch < Step::Fetch
|
|
9
|
-
step_name "markdown-links"
|
|
10
|
-
def call(config:, args:, **)
|
|
11
|
-
_ = args
|
|
12
|
-
links = config["bytes"].to_s.scan(%r{\[([^\]]+)\]\((https?://[^)\s]+)\)}).map do |text, href|
|
|
13
|
-
{ "text" => text, "href" => href }
|
|
14
|
-
end
|
|
15
|
-
{ _meta: {}, body: YAML.dump(links) }
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rexml/document"
|
|
4
|
-
require "yaml"
|
|
5
|
-
|
|
6
|
-
module Textus
|
|
7
|
-
module Step
|
|
8
|
-
module Builtin
|
|
9
|
-
class RssFetch < Step::Fetch
|
|
10
|
-
step_name "rss"
|
|
11
|
-
def call(config:, args:, **)
|
|
12
|
-
_ = args
|
|
13
|
-
doc = REXML::Document.new(config["bytes"].to_s)
|
|
14
|
-
items = doc.elements.to_a("//item").map do |item|
|
|
15
|
-
{
|
|
16
|
-
"title" => item.elements["title"]&.text,
|
|
17
|
-
"link" => item.elements["link"]&.text,
|
|
18
|
-
"pubDate" => item.elements["pubDate"]&.text,
|
|
19
|
-
}
|
|
20
|
-
end
|
|
21
|
-
{ _meta: {}, body: YAML.dump(items) }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
data/lib/textus/step/builtin.rb
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Step
|
|
5
|
-
# Registers the framework-provided fetch steps (json/csv/markdown-links/
|
|
6
|
-
# ical-events/rss) into a registry at Store construction. The successor to
|
|
7
|
-
# Step::Builtin.register_all.
|
|
8
|
-
module Builtin
|
|
9
|
-
STEPS = [
|
|
10
|
-
JsonFetch, CsvFetch, MarkdownLinksFetch, IcalEventsFetch, RssFetch
|
|
11
|
-
].freeze
|
|
12
|
-
|
|
13
|
-
def self.register_all(registry)
|
|
14
|
-
STEPS.each do |klass|
|
|
15
|
-
step = klass.new
|
|
16
|
-
step.name = klass.step_name
|
|
17
|
-
registry.register(step)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|