textus 0.52.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- 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/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- 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 +11 -11
- 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/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.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 +17 -9
- 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 +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- 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 +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- 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 +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- 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 +7 -5
- 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/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- 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/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 -3
- 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 +132 -118
- 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/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/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/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/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/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 -73
- 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/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
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# Drains the job queue: lease a job, look up its handler in the registry, run
|
|
4
|
-
# it (as the job's stamped authority — wired in a later phase), then ack on
|
|
5
|
-
# success or fail (requeue/dead-letter) on a raise. `drain` runs until the
|
|
6
|
-
# queue is empty and returns a summary. Delivery is at-least-once.
|
|
7
|
-
class Worker
|
|
8
|
-
Summary = Struct.new(:completed, :failed, keyword_init: true)
|
|
9
|
-
|
|
10
|
-
# The standard convergence worker: the closed handler allow-list plus the
|
|
11
|
-
# lease TTL from worker_config. Both `drain` and `serve` build it this way.
|
|
12
|
-
def self.for(container:, queue:)
|
|
13
|
-
new(
|
|
14
|
-
queue: queue, registry: Textus::Jobs::Handlers.registry,
|
|
15
|
-
container: container, lease_ttl: container.manifest.data.worker_config[:lease_ttl]
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def initialize(queue:, registry:, container:, lease_ttl: 60)
|
|
20
|
-
@queue = queue
|
|
21
|
-
@registry = registry
|
|
22
|
-
@container = container
|
|
23
|
-
@lease_ttl = lease_ttl
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def drain(worker_id: "drain-#{Process.pid}")
|
|
27
|
-
completed = 0
|
|
28
|
-
failed = 0
|
|
29
|
-
loop do
|
|
30
|
-
leased = @queue.lease(worker_id: worker_id, lease_ttl: @lease_ttl)
|
|
31
|
-
break unless leased
|
|
32
|
-
|
|
33
|
-
case run_one(leased)
|
|
34
|
-
when :completed then completed += 1
|
|
35
|
-
when :dead_lettered then failed += 1
|
|
36
|
-
# :requeued -> a transient failure; it re-leases on a later iteration
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
Summary.new(completed: completed, failed: failed)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def drain_pool(pool: 4)
|
|
43
|
-
summaries = []
|
|
44
|
-
mutex = Mutex.new
|
|
45
|
-
threads = Array.new(pool) do |i|
|
|
46
|
-
Thread.new do
|
|
47
|
-
s = drain(worker_id: "pool-#{Process.pid}-#{i}")
|
|
48
|
-
mutex.synchronize { summaries << s }
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
threads.each(&:join)
|
|
52
|
-
Summary.new(
|
|
53
|
-
completed: summaries.sum(&:completed),
|
|
54
|
-
failed: summaries.sum(&:failed),
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Returns :completed on ack, or the queue's failure verdict (:requeued |
|
|
61
|
-
# :dead_lettered) on a raise. A requeued job re-leases on the next loop
|
|
62
|
-
# iteration, so a transient failure still drains; only a dead-letter is a
|
|
63
|
-
# terminal failure that counts toward the summary.
|
|
64
|
-
def run_one(leased)
|
|
65
|
-
entry = @registry.lookup(leased.job.type)
|
|
66
|
-
entry.handler.call(job: leased.job, container: @container)
|
|
67
|
-
@queue.ack(leased)
|
|
68
|
-
:completed
|
|
69
|
-
rescue StandardError => e
|
|
70
|
-
@queue.fail(leased, error: e.message)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require "yaml"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Maintenance
|
|
5
|
-
# Rename a zone — rewrites the manifest's zones[] entry, rewrites
|
|
6
|
-
# the `zone:` field on every entry under the old zone, and moves
|
|
7
|
-
# every file from zones/<old>/ to zones/<new>/.
|
|
8
|
-
class ZoneMv
|
|
9
|
-
extend Textus::Contract::DSL
|
|
10
|
-
|
|
11
|
-
verb :zone_mv
|
|
12
|
-
summary "Rename a zone — manifest + files. Refuses if destination exists."
|
|
13
|
-
surfaces :cli, :mcp
|
|
14
|
-
cli "zone mv"
|
|
15
|
-
arg :from, String, required: true, positional: true, description: "current zone name"
|
|
16
|
-
arg :to, String, required: true, positional: true, description: "new zone name; refused if a zone by this name already exists"
|
|
17
|
-
arg :dry_run, :boolean, default: false,
|
|
18
|
-
description: "when true, returns the planned zone move without applying it; " \
|
|
19
|
-
"defaults to false, so omitting it applies the move immediately"
|
|
20
|
-
view { |v, _i| v.to_h }
|
|
21
|
-
|
|
22
|
-
def initialize(container:, call:)
|
|
23
|
-
@container = container
|
|
24
|
-
@call = call
|
|
25
|
-
@manifest = container.manifest
|
|
26
|
-
@root = container.root
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def call(from, to, dry_run: false)
|
|
30
|
-
raise UsageError.new("from and to required") if from.nil? || to.nil? || from.empty? || to.empty?
|
|
31
|
-
raise UsageError.new("zone '#{from}' not declared") unless @manifest.data.declared_zone_kinds.key?(from)
|
|
32
|
-
|
|
33
|
-
dest_dir = File.join(@root, "zones", to)
|
|
34
|
-
raise UsageError.new("destination 'zones/#{to}' already exists") if File.exist?(dest_dir)
|
|
35
|
-
|
|
36
|
-
affected_keys = @manifest.data.entries.select { |e| e.zone == from }.map(&:key)
|
|
37
|
-
|
|
38
|
-
steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
|
|
39
|
-
steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
|
|
40
|
-
|
|
41
|
-
plan = Plan.new(steps: steps, warnings: [])
|
|
42
|
-
return plan if dry_run
|
|
43
|
-
|
|
44
|
-
rewrite_manifest!(from, to)
|
|
45
|
-
FileUtils.mv(File.join(@root, "zones", from), dest_dir)
|
|
46
|
-
plan
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def rewrite_manifest!(from, to)
|
|
52
|
-
path = File.join(@root, "manifest.yaml")
|
|
53
|
-
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
54
|
-
raw["zones"].each { |z| z["name"] = to if z["name"] == from }
|
|
55
|
-
raw["entries"].each do |e|
|
|
56
|
-
e["zone"] = to if e["zone"] == from
|
|
57
|
-
e["key"] = e["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
|
|
58
|
-
e["path"] = e["path"].sub(%r{\A#{Regexp.escape(from)}(/|\z)}, "#{to}\\1")
|
|
59
|
-
end
|
|
60
|
-
File.write(path, YAML.dump(raw))
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
data/lib/textus/maintenance.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
# Bulk and structural changes to a textus store. Each use case returns
|
|
3
|
-
# a Plan when called with dry_run: true, and applies the plan when
|
|
4
|
-
# called with dry_run: false.
|
|
5
|
-
module Maintenance
|
|
6
|
-
# A Plan is a JSON-shaped preview. Steps are op-tagged hashes the
|
|
7
|
-
# use case knows how to apply. Warnings are strings surfaced to
|
|
8
|
-
# the operator (skipped keys, ambiguities).
|
|
9
|
-
Plan = Data.define(:steps, :warnings) do
|
|
10
|
-
def to_h
|
|
11
|
-
{ "steps" => steps, "warnings" => warnings }
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/textus/mcp/catalog.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Derives the entire MCP tool surface from the per-verb contracts
|
|
4
|
-
# (ADR 0039). `tool_schemas` feeds tools/list; `call` is the generic
|
|
5
|
-
# tools/call dispatch: map JSON args -> (positional, keyword) per the
|
|
6
|
-
# contract, invoke the verb through the role scope, then shape the
|
|
7
|
-
# return value with the contract's default view. No per-tool code.
|
|
8
|
-
module Catalog
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
# Contracts of every MCP-surfaced verb, in Dispatcher order.
|
|
12
|
-
def specs
|
|
13
|
-
Textus::Dispatcher::VERBS.values
|
|
14
|
-
.select { |k| mcp_surfaced?(k) }
|
|
15
|
-
.map(&:contract)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def tool_schemas
|
|
19
|
-
specs.map do |s|
|
|
20
|
-
{ name: s.verb.to_s, description: s.summary, inputSchema: s.input_schema }
|
|
21
|
-
end.freeze
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def names
|
|
25
|
-
specs.map { |s| s.verb.to_s }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
29
|
-
# real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
|
|
30
|
-
# from this so it can never advertise a verb the agent cannot call, nor
|
|
31
|
-
# omit one it can (ADR 0056). Excludes Write/Maintenance.
|
|
32
|
-
def read_verbs
|
|
33
|
-
Textus::Dispatcher::VERBS
|
|
34
|
-
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Read::") }
|
|
35
|
-
.keys.map(&:to_s)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
|
|
39
|
-
# read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
|
|
40
|
-
# from this so it advertises bare verb names the agent can call (no `--as`/
|
|
41
|
-
# `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
|
|
42
|
-
# (ADR 0056, ADR 0057).
|
|
43
|
-
def write_verbs
|
|
44
|
-
Textus::Dispatcher::VERBS
|
|
45
|
-
.select { |_verb, klass| mcp_surfaced?(klass) && klass.name.start_with?("Textus::Write::") }
|
|
46
|
-
.keys.map(&:to_s)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def mcp_surfaced?(klass)
|
|
50
|
-
klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def call(name, session:, store:, args:)
|
|
54
|
-
klass = Textus::Dispatcher::VERBS[name.to_sym]
|
|
55
|
-
raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
|
|
56
|
-
|
|
57
|
-
spec = klass.contract
|
|
58
|
-
inputs = Textus::Contract::Binder.inputs_from_wire(spec, args)
|
|
59
|
-
result = store.as(session.role).dispatch_bound(spec.verb, inputs, session: session)
|
|
60
|
-
Textus::Contract::View.render(spec, :default, result, inputs)
|
|
61
|
-
rescue Textus::Contract::MissingArgs => e
|
|
62
|
-
raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
|
|
63
|
-
rescue ContractDrift, CursorExpired
|
|
64
|
-
raise
|
|
65
|
-
rescue Textus::Error => e
|
|
66
|
-
raise ToolError.new("#{name}: #{e.message}")
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
data/lib/textus/mcp/errors.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module MCP
|
|
3
|
-
# Manifest fingerprint changed mid-session. Client should re-boot.
|
|
4
|
-
class ContractDrift < Textus::Error
|
|
5
|
-
JSONRPC_CODE = -32_001
|
|
6
|
-
|
|
7
|
-
def initialize(message, details: {})
|
|
8
|
-
super("contract_drift", message, details: details)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Audit cursor fell off the keep window. Client should re-boot and
|
|
13
|
-
# resume from the new latest_seq.
|
|
14
|
-
class CursorExpired < Textus::Error
|
|
15
|
-
JSONRPC_CODE = -32_002
|
|
16
|
-
|
|
17
|
-
def initialize(message, details: {})
|
|
18
|
-
super("cursor_expired", message, details: details)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
23
|
-
# underlying Textus::Error or generic StandardError.
|
|
24
|
-
class ToolError < Textus::Error
|
|
25
|
-
JSONRPC_CODE = -32_000
|
|
26
|
-
|
|
27
|
-
def initialize(message, details: {})
|
|
28
|
-
super("tool_error", message, details: details)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
data/lib/textus/mcp/server.rb
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module MCP
|
|
5
|
-
# Stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05. One line per
|
|
6
|
-
# message (NDJSON). Holds a single Session for the lifetime of stdin.
|
|
7
|
-
class Server
|
|
8
|
-
PROTOCOL_VERSION = "2024-11-05"
|
|
9
|
-
SERVER_INFO = { "name" => "textus", "version" => Textus::VERSION }.freeze
|
|
10
|
-
|
|
11
|
-
def initialize(store:, stdin: $stdin, stdout: $stdout, role: Textus::Role::DEFAULT)
|
|
12
|
-
@store = store
|
|
13
|
-
@stdin = stdin
|
|
14
|
-
@stdout = stdout
|
|
15
|
-
@role = role
|
|
16
|
-
@session = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def run
|
|
20
|
-
@stdin.each_line do |line|
|
|
21
|
-
line = line.strip
|
|
22
|
-
next if line.empty?
|
|
23
|
-
|
|
24
|
-
handle_line(line)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def handle_line(line)
|
|
31
|
-
msg = JSON.parse(line)
|
|
32
|
-
rescue JSON::ParserError => e
|
|
33
|
-
emit_error(nil, -32_700, "parse error: #{e.message}")
|
|
34
|
-
else
|
|
35
|
-
dispatch(msg)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def dispatch(msg)
|
|
39
|
-
rid = msg["id"]
|
|
40
|
-
case msg["method"]
|
|
41
|
-
when "initialize" then handle_initialize(rid, msg["params"] || {})
|
|
42
|
-
when "tools/list" then handle_tools_list(rid)
|
|
43
|
-
when "tools/call" then handle_tools_call(rid, msg["params"] || {})
|
|
44
|
-
when "ping" then emit_result(rid, {})
|
|
45
|
-
when "shutdown" then emit_result(rid, nil)
|
|
46
|
-
when "notifications/initialized" then nil
|
|
47
|
-
else emit_error(rid, -32_601, "method not found: #{msg["method"]}")
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def handle_initialize(rid, _params)
|
|
52
|
-
# The acting role IS the resolved connection role (ADR 0040): the MCP
|
|
53
|
-
# transport defaults to `agent`, which can write the queue, so its
|
|
54
|
-
# propose_zone resolves directly. If a connection's role cannot propose,
|
|
55
|
-
# propose_zone is nil and the `propose` tool reports that honestly.
|
|
56
|
-
propose_zone = @store.manifest.policy.propose_zone_for(@role)
|
|
57
|
-
|
|
58
|
-
@session = Session.new(
|
|
59
|
-
role: @role,
|
|
60
|
-
cursor: @store.audit_log.latest_seq,
|
|
61
|
-
propose_zone: propose_zone,
|
|
62
|
-
contract_etag: contract_etag,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# ADR 0075: announce the connection to connect-time hooks with the
|
|
66
|
-
# resolved role. Distinct from :store_loaded (fired at Store.new under
|
|
67
|
-
# the default role, before any connection's role is known).
|
|
68
|
-
@store.events.publish(
|
|
69
|
-
:session_opened,
|
|
70
|
-
ctx: Hooks::Context.new(scope: @store.as(@role)),
|
|
71
|
-
role: @role,
|
|
72
|
-
cursor: @session.cursor,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
emit_result(rid, {
|
|
76
|
-
"protocolVersion" => PROTOCOL_VERSION,
|
|
77
|
-
"serverInfo" => SERVER_INFO,
|
|
78
|
-
"capabilities" => { "tools" => {} },
|
|
79
|
-
})
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def handle_tools_list(rid)
|
|
83
|
-
emit_result(rid, { "tools" => Catalog.tool_schemas })
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def handle_tools_call(rid, params)
|
|
87
|
-
unless @session
|
|
88
|
-
emit_error(rid, -32_002, "session not initialized; call 'initialize' first")
|
|
89
|
-
return
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
name = params["name"]
|
|
93
|
-
args = params["arguments"] || {}
|
|
94
|
-
|
|
95
|
-
# ADR 0083: the contract-drift guard gates mutating verbs — every MCP
|
|
96
|
-
# verb that is NOT a pure read (Write:: + the destructive Maintenance::
|
|
97
|
-
# verbs drain/zone_mv/key_*_prefix). Reads and boot bypass it (a stale
|
|
98
|
-
# read returns on-disk truth; boot re-orients). Keying on read_verbs
|
|
99
|
-
# (not write_verbs) keeps the destructive Maintenance:: verbs gated.
|
|
100
|
-
@session.check_etag!(contract_etag) unless Catalog.read_verbs.include?(name)
|
|
101
|
-
|
|
102
|
-
result = Catalog.call(name, session: @session, store: @store, args: args)
|
|
103
|
-
@session = @session.advance_cursor(@store.audit_log.latest_seq) if name == "pulse"
|
|
104
|
-
@session = @session.with(contract_etag: contract_etag) if name == "boot"
|
|
105
|
-
|
|
106
|
-
emit_result(rid, {
|
|
107
|
-
"content" => [{ "type" => "text", "text" => JSON.dump(result) }],
|
|
108
|
-
"isError" => false,
|
|
109
|
-
})
|
|
110
|
-
rescue ContractDrift => e
|
|
111
|
-
emit_error(rid, ContractDrift::JSONRPC_CODE, e.message)
|
|
112
|
-
rescue CursorExpired => e
|
|
113
|
-
emit_error(rid, CursorExpired::JSONRPC_CODE, e.message)
|
|
114
|
-
rescue ToolError => e
|
|
115
|
-
emit_error(rid, ToolError::JSONRPC_CODE, e.message)
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
emit_error(rid, -32_603, "internal: #{e.class}: #{e.message}")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def contract_etag
|
|
121
|
-
Textus::Etag.for_contract(@store.root)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def emit_result(rid, result)
|
|
125
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "result" => result })
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def emit_error(rid, code, message)
|
|
129
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "error" => { "code" => code, "message" => message } })
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def write(obj)
|
|
133
|
-
@stdout.puts(JSON.dump(obj))
|
|
134
|
-
@stdout.flush
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
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,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,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Produce
|
|
3
|
-
# Single home for the fetch lifecycle event vocabulary (ADR 0048 D5).
|
|
4
|
-
# Produce::Acquire::Intake (the ingest executor driven by converge + hook) emits through
|
|
5
|
-
# this seam so the event names and payload shapes live in one place with one
|
|
6
|
-
# derived hook context.
|
|
7
|
-
class Events
|
|
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(:entry_fetch_started, ctx: @hook_context, key: key, mode: mode)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def failed(key, error)
|
|
25
|
-
@events.publish(:entry_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
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|