textus 0.51.0 → 0.53.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 +37 -0
- data/README.md +74 -66
- data/SPEC.md +100 -223
- data/docs/architecture/README.md +91 -80
- data/docs/reference/conventions.md +8 -8
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/background/retention/apply.rb +50 -0
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +59 -49
- data/lib/textus/command.rb +36 -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 +13 -13
- 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 +2 -2
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- 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/sentinels.rb +2 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +19 -11
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +40 -36
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +93 -76
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +21 -0
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/data.rb +30 -15
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- 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 +4 -4
- data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +18 -12
- data/lib/textus/manifest/schema/validator.rb +27 -25
- data/lib/textus/manifest/schema/vocabulary.rb +3 -3
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +17 -63
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/ports/build_lock.rb +1 -1
- data/lib/textus/ports/queue/job.rb +65 -0
- data/lib/textus/ports/queue.rb +130 -0
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/role.rb +1 -1
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -4
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- 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 +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -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 +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +133 -107
- 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.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -52
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- 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/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/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/reconcile.rb +0 -160
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- 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/produce_on_write_subscriber.rb +0 -69
- data/lib/textus/produce/events.rb +0 -36
- 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/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/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
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
require "timeout"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Pipeline
|
|
5
5
|
module Acquire
|
|
6
6
|
# Internal ingest executor for one machine-zone intake entry. No longer a
|
|
7
7
|
# public verb (ADR 0079 collapsed the `fetch` surface): used by the
|
|
8
|
-
#
|
|
8
|
+
# converge sweep (drain/serve) and `textus hook run` only — ingest is system-pushed
|
|
9
9
|
# (ADR 0089 removed the read-through that once also drove it).
|
|
10
10
|
class Intake
|
|
11
|
-
FETCH_TIMEOUT_SECONDS = Textus::
|
|
11
|
+
FETCH_TIMEOUT_SECONDS = Textus::Pipeline::Acquire::Handler::FETCH_TIMEOUT_SECONDS
|
|
12
12
|
|
|
13
13
|
def initialize(container:, call:)
|
|
14
14
|
@container = container
|
|
15
15
|
@call = call
|
|
16
16
|
@manifest = container.manifest
|
|
17
17
|
@schemas = container.schemas
|
|
18
|
-
@
|
|
18
|
+
@steps = container.steps
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# call(key) is the primary entry; run is kept as an alias for
|
|
@@ -61,10 +61,6 @@ module Textus
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
-
def fetch_events
|
|
65
|
-
@fetch_events ||= Textus::Produce::Events.from(container: @container, call: @call)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
64
|
# ADR 0079: a per-rule fetch_timeout_seconds override was an accepted loss
|
|
69
65
|
# in the fetch:/retention: → lifecycle: collapse; the constant ceiling
|
|
70
66
|
# applies to every intake.
|
|
@@ -72,36 +68,35 @@ module Textus
|
|
|
72
68
|
FETCH_TIMEOUT_SECONDS
|
|
73
69
|
end
|
|
74
70
|
|
|
71
|
+
def steps_ctx
|
|
72
|
+
@steps_ctx ||= Textus::Step::Context.for(container: @container, call: @call)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
75
|
def fetch_with_events(key, mentry, remaining)
|
|
76
|
-
|
|
76
|
+
@steps.publish(:entry_fetch_started, ctx: steps_ctx, key: key, mode: :sync)
|
|
77
77
|
call_intake(key, mentry, remaining)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def call_intake(key, mentry, remaining)
|
|
81
|
-
Textus::
|
|
81
|
+
Textus::Pipeline::Acquire::Handler.invoke(
|
|
82
82
|
caps: @container, handler: mentry.handler,
|
|
83
83
|
config: mentry.config,
|
|
84
84
|
args: { trigger_key: key, leaf_segments: remaining || [] },
|
|
85
85
|
label: "intake", timeout: fetch_timeout_for(key)
|
|
86
86
|
)
|
|
87
87
|
rescue Textus::Error => e
|
|
88
|
-
|
|
88
|
+
@steps.publish(:entry_fetch_failed, ctx: steps_ctx, key: key,
|
|
89
|
+
error_class: e.class.name, error_message: e.message)
|
|
89
90
|
raise
|
|
90
91
|
rescue StandardError => e
|
|
91
|
-
|
|
92
|
+
@steps.publish(:entry_fetch_failed, ctx: steps_ctx, key: key,
|
|
93
|
+
error_class: e.class.name, error_message: e.message)
|
|
92
94
|
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def persist_and_notify(key, mentry, result, before_etag)
|
|
96
98
|
normalized = self.class.normalize_action_result(result, format: mentry.format)
|
|
97
|
-
|
|
98
|
-
manifest: @manifest, schemas: @schemas,
|
|
99
|
-
).for(:reconcile, key).check!(
|
|
100
|
-
Textus::Domain::Policy::Evaluation.new(
|
|
101
|
-
actor: @call.role, transition: :reconcile, origin: nil,
|
|
102
|
-
target: key, envelope: nil, manifest: @manifest
|
|
103
|
-
),
|
|
104
|
-
)
|
|
99
|
+
auth.check_action!(action: :converge, actor: @call.role, key: key)
|
|
105
100
|
envelope = writer.put(
|
|
106
101
|
key,
|
|
107
102
|
mentry: mentry,
|
|
@@ -110,7 +105,10 @@ module Textus
|
|
|
110
105
|
),
|
|
111
106
|
)
|
|
112
107
|
change = detect_change(before_etag, envelope)
|
|
113
|
-
|
|
108
|
+
unless change == :unchanged
|
|
109
|
+
@steps.publish(:entry_fetched, ctx: steps_ctx, key: key,
|
|
110
|
+
envelope: envelope, change: change)
|
|
111
|
+
end
|
|
114
112
|
envelope
|
|
115
113
|
end
|
|
116
114
|
|
|
@@ -124,6 +122,10 @@ module Textus
|
|
|
124
122
|
def writer
|
|
125
123
|
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
126
124
|
end
|
|
125
|
+
|
|
126
|
+
def auth
|
|
127
|
+
@auth ||= Textus::Gate::Auth.new(@container)
|
|
128
|
+
end
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Pipeline
|
|
5
5
|
module Acquire
|
|
6
6
|
# Builds an entry's DATA artifact (ADR 0094) by running the projection
|
|
7
7
|
# pipeline; rendering is a publish concern. External entries are NOT built
|
|
@@ -34,13 +34,13 @@ module Textus
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
Deps = Data.define(:manifest, :reader, :lister, :
|
|
37
|
+
Deps = Data.define(:manifest, :reader, :lister, :steps, :transform_context)
|
|
38
38
|
|
|
39
39
|
def self.renderers
|
|
40
40
|
@renderers ||= {
|
|
41
|
-
"text" =>
|
|
42
|
-
"json" =>
|
|
43
|
-
"yaml" =>
|
|
41
|
+
"text" => Textus::Pipeline::Acquire::Serializer::Text,
|
|
42
|
+
"json" => Textus::Pipeline::Acquire::Serializer::Json,
|
|
43
|
+
"yaml" => Textus::Pipeline::Acquire::Serializer::Yaml,
|
|
44
44
|
}
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -49,14 +49,16 @@ module Textus
|
|
|
49
49
|
@call = call
|
|
50
50
|
@manifest = container.manifest
|
|
51
51
|
@file_store = container.file_store
|
|
52
|
-
@
|
|
52
|
+
@steps = container.steps
|
|
53
53
|
@root = container.root
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Runs the projection pipeline for `mentry` and returns the on-disk
|
|
57
57
|
# target_path string.
|
|
58
58
|
def run(mentry)
|
|
59
|
-
reader
|
|
59
|
+
reader = lambda do |key|
|
|
60
|
+
Textus::Action::Get.new(key: key).call(container: @container, call: @call)
|
|
61
|
+
end
|
|
60
62
|
# Projections must be able to read source data from any nested entry,
|
|
61
63
|
# including keyless (publish_tree) ones like knowledge.decisions.
|
|
62
64
|
# The `include_keyless: true` option makes the resolver walk those dirs
|
|
@@ -64,15 +66,15 @@ module Textus
|
|
|
64
66
|
resolver = @manifest.resolver
|
|
65
67
|
lister = lambda do |prefix:|
|
|
66
68
|
resolver.enumerate(prefix: prefix, include_keyless: true)
|
|
67
|
-
.map { |row| { "key" => row[:key], "
|
|
69
|
+
.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } }
|
|
68
70
|
end
|
|
69
71
|
self.class.pipeline_run(
|
|
70
72
|
mentry: mentry,
|
|
71
73
|
deps: Deps.new(
|
|
72
74
|
manifest: @manifest,
|
|
73
|
-
reader: reader
|
|
75
|
+
reader: reader,
|
|
74
76
|
lister: lister,
|
|
75
|
-
|
|
77
|
+
steps: @steps,
|
|
76
78
|
transform_context: @container,
|
|
77
79
|
),
|
|
78
80
|
)
|
|
@@ -95,7 +97,7 @@ module Textus
|
|
|
95
97
|
reader: deps.reader,
|
|
96
98
|
spec: mentry.source.projection_spec,
|
|
97
99
|
lister: deps.lister,
|
|
98
|
-
|
|
100
|
+
steps: deps.steps,
|
|
99
101
|
transform_context: deps.transform_context,
|
|
100
102
|
).run
|
|
101
103
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Pipeline
|
|
5
5
|
module Acquire
|
|
6
6
|
class Serializer
|
|
7
7
|
class Json < Serializer
|
|
8
8
|
def call(mentry:, data:)
|
|
9
9
|
content = default_shape(mentry, data)
|
|
10
|
-
final =
|
|
10
|
+
final = Textus::Pipeline::Acquire::Projection::InjectMeta.call(content, mentry)
|
|
11
11
|
Entry.for_format("json").serialize(meta: {}, body: "", content: final)
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Pipeline
|
|
5
5
|
module Acquire
|
|
6
6
|
class Serializer
|
|
7
7
|
class Yaml < Serializer
|
|
8
8
|
def call(mentry:, data:)
|
|
9
9
|
content = default_shape(mentry, data)
|
|
10
|
-
final =
|
|
10
|
+
final = Textus::Pipeline::Acquire::Projection::InjectMeta.call(content, mentry)
|
|
11
11
|
Entry.for_format("yaml").serialize(meta: {}, body: "", content: final)
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
module
|
|
2
|
+
module Pipeline
|
|
3
3
|
# The single convergence engine (ADR 0093/0094). "Make these machine entries
|
|
4
4
|
# current from upstream." Acquire is per-`from`; publish is one uniform
|
|
5
5
|
# `publish_via` entry point for all kinds (ADR 0094):
|
|
@@ -8,17 +8,16 @@ module Textus
|
|
|
8
8
|
# derived (from: command) -> skip the build; publish_via publishes
|
|
9
9
|
# existing store bytes via mode resolution
|
|
10
10
|
# (None when no targets -> skipped)
|
|
11
|
-
# Runs as the
|
|
11
|
+
# Runs as the converge build actor (self-elevating); the passed `call`
|
|
12
12
|
# supplies only correlation_id/dry_run. Callers choose the key set: the
|
|
13
|
-
# write subscriber passes rdeps ∩ derived;
|
|
13
|
+
# write subscriber passes rdeps ∩ derived; the converge pass passes
|
|
14
14
|
# all-derived + stale-intake.
|
|
15
15
|
class Engine
|
|
16
|
-
# Locked + failure-isolated convergence — the
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# writer (ADR 0087 §5 failure isolation, preserved).
|
|
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).
|
|
22
21
|
def self.converge(container:, call:, keys:)
|
|
23
22
|
Textus::Ports::BuildLock.with(root: container.root) do
|
|
24
23
|
new(container: container, call: call).call(keys: keys)
|
|
@@ -26,9 +25,9 @@ module Textus
|
|
|
26
25
|
rescue Textus::BuildInProgress
|
|
27
26
|
nil
|
|
28
27
|
rescue Textus::Error => e
|
|
29
|
-
container.
|
|
28
|
+
container.steps.publish(
|
|
30
29
|
:produce_failed,
|
|
31
|
-
ctx: Textus::
|
|
30
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
32
31
|
keys: keys, error: e.message
|
|
33
32
|
)
|
|
34
33
|
end
|
|
@@ -63,7 +62,7 @@ module Textus
|
|
|
63
62
|
entry = @manifest.resolver.resolve(key).entry
|
|
64
63
|
|
|
65
64
|
if entry.intake?
|
|
66
|
-
Textus::
|
|
65
|
+
Textus::Pipeline::Acquire::Intake.new(container: @container, call: build_call).run(key) # acquire: re-pull
|
|
67
66
|
entry.publish_via(context) # emit any targets
|
|
68
67
|
out[:produced] << key # a fetch is production
|
|
69
68
|
else
|
|
@@ -73,10 +72,10 @@ module Textus
|
|
|
73
72
|
end
|
|
74
73
|
|
|
75
74
|
def build_actor_call
|
|
76
|
-
build_role = @manifest.policy.actor_for("
|
|
75
|
+
build_role = @manifest.policy.actor_for("converge") or
|
|
77
76
|
raise Textus::UsageError.new(
|
|
78
|
-
"no role holds the '
|
|
79
|
-
hint: "declare a role with `can: [
|
|
77
|
+
"no role holds the 'converge' capability",
|
|
78
|
+
hint: "declare a role with `can: [converge]` in .textus/manifest.yaml",
|
|
80
79
|
)
|
|
81
80
|
Textus::Call.build(
|
|
82
81
|
role: build_role,
|
|
@@ -88,56 +87,11 @@ module Textus
|
|
|
88
87
|
def build_context(call)
|
|
89
88
|
Textus::Manifest::Entry::Base::PublishContext.new(
|
|
90
89
|
container: @container, call: call,
|
|
91
|
-
reader:
|
|
90
|
+
reader: lambda { |key|
|
|
91
|
+
Textus::Action::Get.new(key: key).call(container: @container, call: call)
|
|
92
|
+
}
|
|
92
93
|
)
|
|
93
94
|
end
|
|
94
|
-
|
|
95
|
-
# In-process deferral for the async write trigger (ADR 0087/0093).
|
|
96
|
-
# Spawns a tracked thread that runs Produce.converge after the write
|
|
97
|
-
# returns; a one-time at_exit joins
|
|
98
|
-
# all pending threads so a short-lived CLI process cannot exit before an
|
|
99
|
-
# async rebuild completes. The write itself never blocks.
|
|
100
|
-
module AsyncRunner
|
|
101
|
-
@mutex = Mutex.new
|
|
102
|
-
@threads = []
|
|
103
|
-
@hooked = false
|
|
104
|
-
|
|
105
|
-
class << self
|
|
106
|
-
def enqueue(container:, call:, keys:)
|
|
107
|
-
thread = Thread.new { Textus::Produce::Engine.converge(container: container, call: call, keys: keys) }
|
|
108
|
-
track(thread)
|
|
109
|
-
thread
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Block until every spawned async rebuild has finished. Idempotent;
|
|
113
|
-
# safe to call from at_exit and directly from tests.
|
|
114
|
-
def drain
|
|
115
|
-
pending = @mutex.synchronize { @threads.dup }
|
|
116
|
-
pending.each(&:join)
|
|
117
|
-
@mutex.synchronize { @threads.delete_if { |t| !t.alive? } }
|
|
118
|
-
nil
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
def track(thread)
|
|
124
|
-
@mutex.synchronize do
|
|
125
|
-
@threads.delete_if { |t| !t.alive? }
|
|
126
|
-
@threads << thread
|
|
127
|
-
install_drain_hook
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Register the join-before-exit hook exactly once. Guarded by the
|
|
132
|
-
# caller holding @mutex.
|
|
133
|
-
def install_drain_hook
|
|
134
|
-
return if @hooked
|
|
135
|
-
|
|
136
|
-
@hooked = true
|
|
137
|
-
at_exit { drain }
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
95
|
end
|
|
142
96
|
end
|
|
143
97
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
require "mustache"
|
|
2
|
+
|
|
1
3
|
module Textus
|
|
2
|
-
module
|
|
4
|
+
module Pipeline
|
|
3
5
|
# Renders an entry's stored DATA into the bytes for one publish target
|
|
4
6
|
# (ADR 0094). Relocates the Mustache logic that used to live in the
|
|
5
7
|
# build-time Markdown renderer. Provenance is NOT added here — it lives in
|
|
@@ -78,16 +78,42 @@ module Textus
|
|
|
78
78
|
def verify_integrity
|
|
79
79
|
return [] unless File.exist?(@path)
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
[].tap do |out|
|
|
82
|
+
iterate_with_prev_seq do |line, lineno, prev_seq|
|
|
83
|
+
check_line_integrity(line, lineno, prev_seq, out)
|
|
84
|
+
end
|
|
85
85
|
end
|
|
86
|
-
out
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
private
|
|
90
89
|
|
|
90
|
+
def iterate_with_prev_seq
|
|
91
|
+
prev_seq = nil
|
|
92
|
+
File.foreach(@path).with_index(1) do |line, lineno|
|
|
93
|
+
yield line.chomp, lineno, prev_seq
|
|
94
|
+
parsed = parse_row(line.chomp)
|
|
95
|
+
prev_seq = parsed["seq"] if parsed&.dig("seq").is_a?(Integer)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def check_line_integrity(line, lineno, prev_seq, out)
|
|
100
|
+
violation = check_line(line, lineno)
|
|
101
|
+
if violation
|
|
102
|
+
out << violation
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
parsed = parse_row(line)
|
|
107
|
+
return unless parsed && (seq = parsed["seq"]).is_a?(Integer)
|
|
108
|
+
return unless prev_seq && seq != prev_seq + 1
|
|
109
|
+
|
|
110
|
+
out << {
|
|
111
|
+
"lineno" => lineno,
|
|
112
|
+
"reason" => "seq_gap",
|
|
113
|
+
"detail" => "expected #{prev_seq + 1}, got #{seq}",
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
91
117
|
def rotated(n)
|
|
92
118
|
File.join(Textus::Layout.audit_dir(@root), "audit.log.#{n}")
|
|
93
119
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Ports
|
|
5
5
|
# Writes an "event_error" audit row when a user hook raises during
|
|
6
|
-
#
|
|
6
|
+
# Step::EventBus publish. Attached at Store boot.
|
|
7
7
|
#
|
|
8
|
-
# Integration: uses
|
|
8
|
+
# Integration: uses Step::EventBus#on_error callback (chosen over a
|
|
9
9
|
# synthetic :hook_error event because the bus already owns the
|
|
10
10
|
# rescue and the failure is a bus-internal concern, not a domain
|
|
11
11
|
# event subscribers should be able to filter by key glob).
|
|
@@ -19,8 +19,8 @@ module Textus
|
|
|
19
19
|
@audit_log = audit_log
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def attach(
|
|
23
|
-
|
|
22
|
+
def attach(registry)
|
|
23
|
+
registry.on_error do |event:, hook:, key:, kwargs:, error:|
|
|
24
24
|
record_error(event: event, hook: hook, key: key, kwargs: kwargs, error: error)
|
|
25
25
|
end
|
|
26
26
|
self
|
|
@@ -5,7 +5,7 @@ require "time"
|
|
|
5
5
|
module Textus
|
|
6
6
|
module Ports
|
|
7
7
|
# Cross-process build lock: a pid/host-stamped lockfile under the store root
|
|
8
|
-
# that serializes
|
|
8
|
+
# that serializes converge's produce/sweep. An instantiable class — it holds
|
|
9
9
|
# the root and lock state; `self.with(root:)` is a convenience that constructs
|
|
10
10
|
# one and runs the block under the held lock. It already satisfied ADR 0109's
|
|
11
11
|
# single-shape rule (every port is an instantiable class) before that ADR's
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Textus
|
|
5
|
+
module Ports
|
|
6
|
+
class Queue
|
|
7
|
+
# A unit of deferred work. Pure data. The id is `<type>:<digest>` where the
|
|
8
|
+
# digest is over the args with sorted keys, so logically-identical enqueues
|
|
9
|
+
# collide on the same id — which is how Queue dedups (file already exists).
|
|
10
|
+
# At-least-once delivery means handlers must be idempotent.
|
|
11
|
+
class Job
|
|
12
|
+
DIGEST_BYTES = 16
|
|
13
|
+
|
|
14
|
+
attr_reader :type, :args, :enqueued_by, :max_attempts
|
|
15
|
+
attr_accessor :attempts, :last_error
|
|
16
|
+
|
|
17
|
+
def initialize(type:, args:, enqueued_by: nil, attempts: 0, max_attempts: 3, last_error: nil)
|
|
18
|
+
@type = type.to_s
|
|
19
|
+
@args = stringify(args)
|
|
20
|
+
@enqueued_by = enqueued_by
|
|
21
|
+
@attempts = attempts
|
|
22
|
+
@max_attempts = max_attempts
|
|
23
|
+
@last_error = last_error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def id
|
|
27
|
+
"#{@type}:#{digest}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
"type" => @type,
|
|
33
|
+
"args" => @args,
|
|
34
|
+
"enqueued_by" => @enqueued_by,
|
|
35
|
+
"attempts" => @attempts,
|
|
36
|
+
"max_attempts" => @max_attempts,
|
|
37
|
+
"last_error" => @last_error,
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.from_h(hash)
|
|
42
|
+
new(
|
|
43
|
+
type: hash["type"],
|
|
44
|
+
args: hash["args"] || {},
|
|
45
|
+
enqueued_by: hash["enqueued_by"],
|
|
46
|
+
attempts: hash["attempts"] || 0,
|
|
47
|
+
max_attempts: hash["max_attempts"] || 3,
|
|
48
|
+
last_error: hash["last_error"],
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def digest
|
|
55
|
+
canonical = JSON.dump(@args.sort.to_h)
|
|
56
|
+
Digest::SHA256.hexdigest(canonical)[0, DIGEST_BYTES]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def stringify(hash)
|
|
60
|
+
hash.transform_keys(&:to_s)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Ports
|
|
7
|
+
# File-backed durable job queue under `<root>/.run/queue/`. Each job state
|
|
8
|
+
# is a directory; a job is one `<id>.json` file. Claiming is an atomic
|
|
9
|
+
# `rename(2)` from ready/ to leased/ — the rename winner owns the job, so a
|
|
10
|
+
# worker pool needs no central lock. Dedup falls out of the id-as-filename:
|
|
11
|
+
# enqueueing an id that already exists is a no-op. ADR 0038 (runtime subtree),
|
|
12
|
+
# ADR 0108 (instantiable port).
|
|
13
|
+
class Queue
|
|
14
|
+
STATES = %i[ready leased done failed].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(root:)
|
|
17
|
+
@root = root
|
|
18
|
+
STATES.each { |s| FileUtils.mkdir_p(Textus::Layout.queue_state(root, s)) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enqueue(job)
|
|
22
|
+
dest = path(:ready, job.id)
|
|
23
|
+
return if File.exist?(dest) # dedup: identical work already queued
|
|
24
|
+
|
|
25
|
+
write_atomic(dest, job.to_h)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ready_ids
|
|
29
|
+
Dir.children(Textus::Layout.queue_state(@root, :ready)).map { |f| File.basename(f, ".json") }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A claimed job plus the path it lives at, so ack/fail act on this copy.
|
|
33
|
+
Leased = Struct.new(:job, :leased_path, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
def lease(worker_id:, lease_ttl:)
|
|
36
|
+
ready_dir = Textus::Layout.queue_state(@root, :ready)
|
|
37
|
+
Dir.children(ready_dir).each do |name|
|
|
38
|
+
src = File.join(ready_dir, name)
|
|
39
|
+
dst = File.join(Textus::Layout.queue_state(@root, :leased), name)
|
|
40
|
+
begin
|
|
41
|
+
File.rename(src, dst) # atomic claim; loser's rename raises ENOENT
|
|
42
|
+
rescue Errno::ENOENT
|
|
43
|
+
next # another worker won this one
|
|
44
|
+
end
|
|
45
|
+
job = Job.from_h(JSON.parse(File.read(dst)))
|
|
46
|
+
stamp_lease(dst, worker_id: worker_id, expires_at: Time.now.utc + lease_ttl)
|
|
47
|
+
return Leased.new(job: job, leased_path: dst)
|
|
48
|
+
end
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ack(leased)
|
|
53
|
+
dest = File.join(Textus::Layout.queue_state(@root, :done), File.basename(leased.leased_path))
|
|
54
|
+
File.rename(leased.leased_path, dest)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Increment attempts and either requeue (transient) or dead-letter (attempts
|
|
58
|
+
# exhausted). Returns :requeued or :dead_lettered so the worker can count
|
|
59
|
+
# terminal failures distinctly from retries.
|
|
60
|
+
def fail(leased, error:)
|
|
61
|
+
job = leased.job
|
|
62
|
+
job.attempts += 1
|
|
63
|
+
job.last_error = error
|
|
64
|
+
dead = job.attempts >= job.max_attempts
|
|
65
|
+
write_atomic(path(dead ? :failed : :ready, job.id), job.to_h)
|
|
66
|
+
File.delete(leased.leased_path)
|
|
67
|
+
dead ? :dead_lettered : :requeued
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Return expired leases to ready/ (the holding worker crashed). Returns the
|
|
71
|
+
# count reclaimed. At-least-once delivery: a job whose handler actually
|
|
72
|
+
# finished but whose ack was lost will re-run — handlers must be idempotent.
|
|
73
|
+
def reclaim(now:)
|
|
74
|
+
leased_dir = Textus::Layout.queue_state(@root, :leased)
|
|
75
|
+
count = 0
|
|
76
|
+
Dir.children(leased_dir).each do |name|
|
|
77
|
+
src = File.join(leased_dir, name)
|
|
78
|
+
data = JSON.parse(File.read(src))
|
|
79
|
+
expires = data.dig("lease", "expires_at")
|
|
80
|
+
next if expires && Time.parse(expires) > now
|
|
81
|
+
|
|
82
|
+
dst = File.join(Textus::Layout.queue_state(@root, :ready), name)
|
|
83
|
+
data.delete("lease")
|
|
84
|
+
File.write(src, JSON.pretty_generate(data))
|
|
85
|
+
File.rename(src, dst)
|
|
86
|
+
count += 1
|
|
87
|
+
rescue Errno::ENOENT
|
|
88
|
+
next # raced with another reclaimer / the worker's ack
|
|
89
|
+
end
|
|
90
|
+
count
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def list(state)
|
|
94
|
+
Dir.children(Textus::Layout.queue_state(@root, state.to_sym)).map { |f| File.basename(f, ".json") }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def retry_failed(job_id)
|
|
98
|
+
src = path(:failed, job_id)
|
|
99
|
+
data = JSON.parse(File.read(src))
|
|
100
|
+
data["attempts"] = 0
|
|
101
|
+
data["last_error"] = nil
|
|
102
|
+
write_atomic(path(:ready, job_id), data)
|
|
103
|
+
File.delete(src)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def purge(state)
|
|
107
|
+
dir = Textus::Layout.queue_state(@root, state.to_sym)
|
|
108
|
+
Dir.children(dir).each { |f| File.delete(File.join(dir, f)) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def stamp_lease(leased_path, worker_id:, expires_at:)
|
|
114
|
+
data = JSON.parse(File.read(leased_path))
|
|
115
|
+
data["lease"] = { "worker_id" => worker_id, "expires_at" => expires_at.iso8601 }
|
|
116
|
+
File.write(leased_path, JSON.pretty_generate(data))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def path(state, job_id)
|
|
120
|
+
File.join(Textus::Layout.queue_state(@root, state), "#{job_id}.json")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def write_atomic(dest, hash)
|
|
124
|
+
tmp = "#{dest}.#{Process.pid}.tmp"
|
|
125
|
+
File.write(tmp, JSON.pretty_generate(hash))
|
|
126
|
+
File.rename(tmp, dest) # atomic on same filesystem
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
# Persistence adapter for sentinel files. Owns the on-disk JSON shape, the
|
|
8
8
|
# path layout (<store_root>/.run/sentinels/<target-rel-to-repo>.textus-managed.json
|
|
9
9
|
# — runtime, git-ignored, ADR 0070), and all File/FileUtils I/O.
|
|
10
|
-
#
|
|
10
|
+
# Core::Sentinel is a pure value object that depends on this port for
|
|
11
11
|
# reads and writes.
|
|
12
12
|
class SentinelStore
|
|
13
13
|
SUFFIX = ".textus-managed.json".freeze
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
|
|
27
27
|
def load(path, repo_root)
|
|
28
28
|
raw = JSON.parse(File.read(path))
|
|
29
|
-
Textus::
|
|
29
|
+
Textus::Core::Sentinel.new(
|
|
30
30
|
target: absolutize(raw["target"], repo_root),
|
|
31
31
|
source: absolutize(raw["source"], repo_root),
|
|
32
32
|
sha256: raw["sha256"],
|