textus 0.40.0 → 0.41.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 +13 -1
- data/SPEC.md +16 -2
- data/docs/architecture/README.md +256 -0
- data/docs/reference/conventions.md +148 -0
- data/lib/textus/cli/verb/hook_run.rb +3 -1
- data/lib/textus/cli/verb/put.rb +1 -1
- data/lib/textus/doctor/check/publish_tree_index_overlap.rb +48 -0
- data/lib/textus/doctor.rb +1 -0
- data/lib/textus/manifest/entry/base.rb +12 -20
- data/lib/textus/manifest/entry/nested.rb +8 -89
- data/lib/textus/manifest/entry/publish/each.rb +83 -0
- data/lib/textus/manifest/entry/publish/each_dir.rb +74 -0
- data/lib/textus/manifest/entry/publish/each_file.rb +29 -0
- data/lib/textus/manifest/entry/publish/mode.rb +39 -0
- data/lib/textus/manifest/entry/publish/none.rb +14 -0
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +72 -0
- data/lib/textus/manifest/entry/publish/template.rb +22 -0
- data/lib/textus/manifest/entry/publish/to_paths.rb +27 -0
- data/lib/textus/manifest/entry/publish/tree.rb +54 -0
- data/lib/textus/manifest/entry/publish.rb +45 -0
- data/lib/textus/manifest/entry/validators/publish.rb +26 -0
- data/lib/textus/manifest/entry/validators.rb +1 -1
- data/lib/textus/manifest/schema.rb +1 -1
- data/lib/textus/ports/publisher.rb +14 -2
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/fetch_events.rb +42 -0
- data/lib/textus/write/fetch_orchestrator.rb +2 -3
- data/lib/textus/write/fetch_worker.rb +13 -22
- data/lib/textus/write/intake_fetch.rb +8 -6
- metadata +16 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +0 -79
data/lib/textus/version.rb
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Write
|
|
3
|
+
# Single home for the fetch lifecycle event vocabulary (ADR 0048 D5). Both
|
|
4
|
+
# FetchWorker (synchronous semantics) and FetchOrchestrator (async policy)
|
|
5
|
+
# emit through this seam so the event names and payload shapes live in one
|
|
6
|
+
# place with one derived hook context.
|
|
7
|
+
class FetchEvents
|
|
8
|
+
def self.from(container:, call:)
|
|
9
|
+
new(
|
|
10
|
+
events: container.events,
|
|
11
|
+
hook_context: Textus::Hooks::Context.for(container: container, call: call),
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(events:, hook_context:)
|
|
16
|
+
@events = events
|
|
17
|
+
@hook_context = hook_context
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def started(key, mode: :sync)
|
|
21
|
+
@events.publish(:fetch_started, ctx: @hook_context, key: key, mode: mode)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failed(key, error)
|
|
25
|
+
@events.publish(:fetch_failed, ctx: @hook_context, key: key,
|
|
26
|
+
error_class: error.class.name, error_message: error.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fetched(key, envelope, change)
|
|
30
|
+
return if change == :unchanged
|
|
31
|
+
|
|
32
|
+
@events.publish(:entry_fetched, ctx: @hook_context, key: key, envelope: envelope, change: change)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def backgrounded(key, started_at:, budget_ms:)
|
|
36
|
+
payload = { key: key, started_at: started_at, budget_ms: budget_ms }
|
|
37
|
+
payload[:ctx] = @hook_context if @hook_context
|
|
38
|
+
@events.publish(:fetch_backgrounded, **payload)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -10,6 +10,7 @@ module Textus
|
|
|
10
10
|
@events = events
|
|
11
11
|
@hook_context = hook_context
|
|
12
12
|
@detached_spawner = detached_spawner || default_spawner
|
|
13
|
+
@fetch_events = Textus::Write::FetchEvents.new(events: @events, hook_context: @hook_context)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def execute(action, key:)
|
|
@@ -82,9 +83,7 @@ module Textus
|
|
|
82
83
|
|
|
83
84
|
probe.release
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
payload[:ctx] = @hook_context if @hook_context
|
|
87
|
-
@events.publish(:fetch_backgrounded, **payload)
|
|
86
|
+
@fetch_events.backgrounded(key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms)
|
|
88
87
|
@detached_spawner.call(store_root: @store_root, key: key)
|
|
89
88
|
Textus::Domain::Outcome::Detached.new
|
|
90
89
|
elsif result.is_a?(Textus::Error)
|
|
@@ -18,7 +18,6 @@ module Textus
|
|
|
18
18
|
@call = call
|
|
19
19
|
@manifest = container.manifest
|
|
20
20
|
@schemas = container.schemas
|
|
21
|
-
@events = container.events
|
|
22
21
|
@rpc = container.rpc
|
|
23
22
|
end
|
|
24
23
|
|
|
@@ -35,7 +34,7 @@ module Textus
|
|
|
35
34
|
remaining = res.remaining
|
|
36
35
|
raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
37
36
|
|
|
38
|
-
before_etag =
|
|
37
|
+
before_etag = @container.file_store.exists?(path) ? @container.file_store.etag(path) : nil
|
|
39
38
|
result = fetch_with_events(key, mentry, remaining)
|
|
40
39
|
persist_and_notify(key, mentry, result, before_etag)
|
|
41
40
|
end
|
|
@@ -65,8 +64,8 @@ module Textus
|
|
|
65
64
|
|
|
66
65
|
private
|
|
67
66
|
|
|
68
|
-
def
|
|
69
|
-
@
|
|
67
|
+
def fetch_events
|
|
68
|
+
@fetch_events ||= FetchEvents.from(container: @container, call: @call)
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
def fetch_timeout_for(key)
|
|
@@ -75,30 +74,22 @@ module Textus
|
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
def fetch_with_events(key, mentry, remaining)
|
|
78
|
-
|
|
77
|
+
fetch_events.started(key)
|
|
79
78
|
call_intake(key, mentry, remaining)
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
def call_intake(key, mentry, remaining)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
end
|
|
90
|
-
rescue Timeout::Error
|
|
91
|
-
@events.publish(:fetch_failed, ctx: hook_context, key: key,
|
|
92
|
-
error_class: "Timeout::Error",
|
|
93
|
-
error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
|
|
94
|
-
raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
|
|
82
|
+
IntakeFetch.invoke(
|
|
83
|
+
caps: @container, handler: mentry.handler,
|
|
84
|
+
config: mentry.config,
|
|
85
|
+
args: { trigger_key: key, leaf_segments: remaining || [] },
|
|
86
|
+
label: "intake", timeout: fetch_timeout_for(key)
|
|
87
|
+
)
|
|
95
88
|
rescue Textus::Error => e
|
|
96
|
-
|
|
97
|
-
error_message: e.message)
|
|
89
|
+
fetch_events.failed(key, e)
|
|
98
90
|
raise
|
|
99
91
|
rescue StandardError => e
|
|
100
|
-
|
|
101
|
-
error_message: e.message)
|
|
92
|
+
fetch_events.failed(key, e)
|
|
102
93
|
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
103
94
|
end
|
|
104
95
|
|
|
@@ -120,7 +111,7 @@ module Textus
|
|
|
120
111
|
),
|
|
121
112
|
)
|
|
122
113
|
change = detect_change(before_etag, envelope)
|
|
123
|
-
|
|
114
|
+
fetch_events.fetched(key, envelope, change)
|
|
124
115
|
envelope
|
|
125
116
|
end
|
|
126
117
|
|
|
@@ -2,18 +2,20 @@ require "timeout"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Write
|
|
5
|
-
# Invokes a :resolve_intake hook handler by name under a timeout
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# Invokes a :resolve_intake hook handler by name under a timeout — the single
|
|
6
|
+
# home for "call the intake handler under a deadline" (ADR 0048 D1). Shared by
|
|
7
|
+
# FetchWorker (the :fetch verb), `textus put --fetch`, and `textus hook run`.
|
|
8
|
+
# Always passes a Container as `caps:` so the hook contract (ADR 0027) is
|
|
9
|
+
# uniform across every entry point. Maps Timeout::Error to a UsageError;
|
|
10
|
+
# leaves any other error to the caller (call sites differ in how they wrap).
|
|
9
11
|
module IntakeFetch
|
|
10
12
|
FETCH_TIMEOUT_SECONDS = 30
|
|
11
13
|
|
|
12
14
|
module_function
|
|
13
15
|
|
|
14
|
-
def invoke(
|
|
16
|
+
def invoke(caps:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
|
|
15
17
|
Timeout.timeout(timeout) do
|
|
16
|
-
rpc.invoke(:resolve_intake, handler, caps:
|
|
18
|
+
caps.rpc.invoke(:resolve_intake, handler, caps: caps, config: config, args: args)
|
|
17
19
|
end
|
|
18
20
|
rescue Timeout::Error
|
|
19
21
|
raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.41.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -106,6 +106,8 @@ files:
|
|
|
106
106
|
- CHANGELOG.md
|
|
107
107
|
- README.md
|
|
108
108
|
- SPEC.md
|
|
109
|
+
- docs/architecture/README.md
|
|
110
|
+
- docs/reference/conventions.md
|
|
109
111
|
- exe/textus
|
|
110
112
|
- lib/textus.rb
|
|
111
113
|
- lib/textus/boot.rb
|
|
@@ -179,6 +181,7 @@ files:
|
|
|
179
181
|
- lib/textus/doctor/check/orphaned_publish_targets.rb
|
|
180
182
|
- lib/textus/doctor/check/proposal_targets.rb
|
|
181
183
|
- lib/textus/doctor/check/protocol_version.rb
|
|
184
|
+
- lib/textus/doctor/check/publish_tree_index_overlap.rb
|
|
182
185
|
- lib/textus/doctor/check/rule_ambiguity.rb
|
|
183
186
|
- lib/textus/doctor/check/schema_parse_error.rb
|
|
184
187
|
- lib/textus/doctor/check/schema_violations.rb
|
|
@@ -257,13 +260,23 @@ files:
|
|
|
257
260
|
- lib/textus/manifest/entry/leaf.rb
|
|
258
261
|
- lib/textus/manifest/entry/nested.rb
|
|
259
262
|
- lib/textus/manifest/entry/parser.rb
|
|
263
|
+
- lib/textus/manifest/entry/publish.rb
|
|
264
|
+
- lib/textus/manifest/entry/publish/each.rb
|
|
265
|
+
- lib/textus/manifest/entry/publish/each_dir.rb
|
|
266
|
+
- lib/textus/manifest/entry/publish/each_file.rb
|
|
267
|
+
- lib/textus/manifest/entry/publish/mode.rb
|
|
268
|
+
- lib/textus/manifest/entry/publish/none.rb
|
|
269
|
+
- lib/textus/manifest/entry/publish/subtree_mirror.rb
|
|
270
|
+
- lib/textus/manifest/entry/publish/template.rb
|
|
271
|
+
- lib/textus/manifest/entry/publish/to_paths.rb
|
|
272
|
+
- lib/textus/manifest/entry/publish/tree.rb
|
|
260
273
|
- lib/textus/manifest/entry/validators.rb
|
|
261
274
|
- lib/textus/manifest/entry/validators/events.rb
|
|
262
275
|
- lib/textus/manifest/entry/validators/format_matrix.rb
|
|
263
276
|
- lib/textus/manifest/entry/validators/ignore.rb
|
|
264
277
|
- lib/textus/manifest/entry/validators/index_filename.rb
|
|
265
278
|
- lib/textus/manifest/entry/validators/inject_boot.rb
|
|
266
|
-
- lib/textus/manifest/entry/validators/
|
|
279
|
+
- lib/textus/manifest/entry/validators/publish.rb
|
|
267
280
|
- lib/textus/manifest/policy.rb
|
|
268
281
|
- lib/textus/manifest/resolver.rb
|
|
269
282
|
- lib/textus/manifest/rules.rb
|
|
@@ -320,6 +333,7 @@ files:
|
|
|
320
333
|
- lib/textus/write/accept.rb
|
|
321
334
|
- lib/textus/write/delete.rb
|
|
322
335
|
- lib/textus/write/fetch_all.rb
|
|
336
|
+
- lib/textus/write/fetch_events.rb
|
|
323
337
|
- lib/textus/write/fetch_orchestrator.rb
|
|
324
338
|
- lib/textus/write/fetch_worker.rb
|
|
325
339
|
- lib/textus/write/intake_fetch.rb
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
class Entry
|
|
4
|
-
module Validators
|
|
5
|
-
module PublishEach
|
|
6
|
-
KNOWN_VARS = %w[leaf basename key ext].freeze
|
|
7
|
-
VAR_RE = /\{([a-z]+)\}/
|
|
8
|
-
REQUIRED_DISCRIMINATOR_VARS = %w[leaf basename key].freeze
|
|
9
|
-
|
|
10
|
-
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
11
|
-
# Use raw to detect misuse on non-nested entries (typed attr stubs nil on Base).
|
|
12
|
-
publish_each = entry.nested? ? entry.publish_each : entry.raw["publish_each"]
|
|
13
|
-
return if publish_each.nil?
|
|
14
|
-
|
|
15
|
-
raise UsageError.new("entry '#{entry.key}': publish_each requires nested: true") unless entry.nested?
|
|
16
|
-
|
|
17
|
-
publish_to = entry.publish_to
|
|
18
|
-
raise UsageError.new("entry '#{entry.key}': publish_to and publish_each are mutually exclusive") unless publish_to.empty?
|
|
19
|
-
raise UsageError.new("entry '#{entry.key}': publish_each must be a string") unless publish_each.is_a?(String)
|
|
20
|
-
|
|
21
|
-
used_vars = publish_each.scan(VAR_RE).flatten
|
|
22
|
-
unknown = used_vars - KNOWN_VARS
|
|
23
|
-
unless unknown.empty?
|
|
24
|
-
raise UsageError.new(
|
|
25
|
-
"entry '#{entry.key}': publish_each uses unknown template variable(s) " \
|
|
26
|
-
"#{unknown.map { |v| "{#{v}}" }.join(", ")}. Known: #{KNOWN_VARS.map { |v| "{#{v}}" }.join(", ")}.",
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
validate_discriminator(entry, used_vars, publish_each)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.validate_discriminator(entry, used_vars, publish_each)
|
|
34
|
-
if entry.index_filename
|
|
35
|
-
forbidden = used_vars & %w[basename ext]
|
|
36
|
-
unless forbidden.empty?
|
|
37
|
-
raise UsageError.new(
|
|
38
|
-
"entry '#{entry.key}': publish_each names a directory " \
|
|
39
|
-
"(index_filename: '#{entry.index_filename}'); {basename}/{ext} are file-only — " \
|
|
40
|
-
"use {leaf} or {key}.",
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
last_segment = publish_each.sub(%r{/\z}, "").split("/").last
|
|
44
|
-
if last_segment == entry.index_filename
|
|
45
|
-
raise UsageError.new(
|
|
46
|
-
"entry '#{entry.key}': directory-leaf publish_each must name the target DIRECTORY, " \
|
|
47
|
-
"not the index file — drop the trailing '/#{entry.index_filename}' " \
|
|
48
|
-
"(the whole leaf subtree is copied into the named directory).",
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
ext = File.extname(last_segment)
|
|
52
|
-
unless ext.empty?
|
|
53
|
-
raise UsageError.new(
|
|
54
|
-
"entry '#{entry.key}': directory-leaf publish_each names a DIRECTORY target, but its " \
|
|
55
|
-
"final segment '#{last_segment}' looks like a file (extension '#{ext}') — " \
|
|
56
|
-
"drop the extension (the whole leaf subtree is copied into the named directory).",
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
return if used_vars.intersect?(%w[leaf key])
|
|
60
|
-
|
|
61
|
-
raise UsageError.new(
|
|
62
|
-
"entry '#{entry.key}': directory-leaf publish_each must reference {leaf} or {key} " \
|
|
63
|
-
"(else every leaf would clobber the same directory).",
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
return if used_vars.intersect?(REQUIRED_DISCRIMINATOR_VARS)
|
|
68
|
-
|
|
69
|
-
raise UsageError.new(
|
|
70
|
-
"entry '#{entry.key}': publish_each must reference at least one of {leaf}, {basename}, or {key} " \
|
|
71
|
-
"(else every leaf would clobber the same target).",
|
|
72
|
-
)
|
|
73
|
-
end
|
|
74
|
-
private_class_method :validate_discriminator
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|