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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +53 -73
  4. data/SPEC.md +2 -5
  5. data/docs/architecture/README.md +149 -159
  6. data/lib/textus/action/accept.rb +0 -9
  7. data/lib/textus/action/audit.rb +0 -2
  8. data/lib/textus/action/blame.rb +0 -2
  9. data/lib/textus/action/boot.rb +2 -11
  10. data/lib/textus/action/data_mv.rb +1 -3
  11. data/lib/textus/action/deps.rb +1 -15
  12. data/lib/textus/action/doctor.rb +0 -2
  13. data/lib/textus/action/drain.rb +4 -6
  14. data/lib/textus/action/enqueue.rb +3 -5
  15. data/lib/textus/action/get.rb +1 -3
  16. data/lib/textus/action/ingest.rb +135 -0
  17. data/lib/textus/action/jobs.rb +1 -3
  18. data/lib/textus/action/key_delete.rb +0 -10
  19. data/lib/textus/action/key_delete_prefix.rb +1 -3
  20. data/lib/textus/action/key_mv.rb +1 -15
  21. data/lib/textus/action/key_mv_prefix.rb +1 -3
  22. data/lib/textus/action/list.rb +0 -2
  23. data/lib/textus/action/propose.rb +0 -2
  24. data/lib/textus/action/published.rb +0 -2
  25. data/lib/textus/action/pulse.rb +7 -38
  26. data/lib/textus/action/put.rb +1 -12
  27. data/lib/textus/action/rdeps.rb +2 -12
  28. data/lib/textus/action/reject.rb +0 -12
  29. data/lib/textus/action/rule_explain.rb +0 -4
  30. data/lib/textus/action/rule_lint.rb +1 -3
  31. data/lib/textus/action/rule_list.rb +0 -4
  32. data/lib/textus/action/schema_envelope.rb +0 -2
  33. data/lib/textus/action/uid.rb +0 -2
  34. data/lib/textus/action/where.rb +0 -2
  35. data/lib/textus/action/write_verb.rb +4 -18
  36. data/lib/textus/boot.rb +40 -109
  37. data/lib/textus/command.rb +6 -1
  38. data/lib/textus/container.rb +21 -5
  39. data/lib/textus/core/freshness/evaluator.rb +18 -34
  40. data/lib/textus/core/freshness/verdict.rb +1 -1
  41. data/lib/textus/doctor/check/notebook_sources.rb +50 -0
  42. data/lib/textus/doctor/check/protocol_version.rb +6 -6
  43. data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
  44. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +54 -0
  45. data/lib/textus/doctor/check.rb +0 -1
  46. data/lib/textus/doctor.rb +3 -42
  47. data/lib/textus/envelope/reader.rb +46 -0
  48. data/lib/textus/envelope/writer.rb +209 -0
  49. data/lib/textus/envelope.rb +19 -6
  50. data/lib/textus/errors.rb +9 -5
  51. data/lib/textus/{entry → format}/base.rb +1 -6
  52. data/lib/textus/{entry → format}/json.rb +1 -4
  53. data/lib/textus/{entry → format}/markdown.rb +2 -6
  54. data/lib/textus/{entry → format}/text.rb +2 -6
  55. data/lib/textus/{entry → format}/yaml.rb +1 -4
  56. data/lib/textus/{entry.rb → format.rb} +9 -10
  57. data/lib/textus/gate/auth.rb +58 -12
  58. data/lib/textus/gate.rb +2 -0
  59. data/lib/textus/init.rb +16 -79
  60. data/lib/textus/jobs/base.rb +23 -0
  61. data/lib/textus/jobs/materialize.rb +20 -0
  62. data/lib/textus/{background → jobs}/plan.rb +1 -1
  63. data/lib/textus/jobs/planner.rb +101 -0
  64. data/lib/textus/jobs/retention.rb +48 -0
  65. data/lib/textus/jobs/sweep.rb +27 -0
  66. data/lib/textus/{background → jobs}/worker.rb +2 -2
  67. data/lib/textus/jobs.rb +15 -0
  68. data/lib/textus/key/path.rb +1 -1
  69. data/lib/textus/manifest/entry/base.rb +4 -19
  70. data/lib/textus/manifest/entry/parser.rb +27 -10
  71. data/lib/textus/manifest/entry/produced.rb +13 -34
  72. data/lib/textus/manifest/entry/publish/to_paths.rb +4 -4
  73. data/lib/textus/manifest/entry/publish.rb +0 -12
  74. data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
  75. data/lib/textus/manifest/entry/validators.rb +0 -1
  76. data/lib/textus/manifest/policy/source.rb +14 -62
  77. data/lib/textus/manifest/policy.rb +2 -5
  78. data/lib/textus/manifest/resolver.rb +2 -2
  79. data/lib/textus/manifest/schema/contract.rb +61 -0
  80. data/lib/textus/manifest/schema/keys.rb +6 -20
  81. data/lib/textus/manifest/schema/semantics.rb +232 -0
  82. data/lib/textus/manifest/schema/validator.rb +24 -302
  83. data/lib/textus/manifest/schema/vocabulary.rb +1 -0
  84. data/lib/textus/manifest/schema.rb +1 -4
  85. data/lib/textus/manifest.rb +1 -3
  86. data/lib/textus/ports/{queue → job_store}/job.rb +1 -1
  87. data/lib/textus/ports/{queue.rb → job_store.rb} +6 -13
  88. data/lib/textus/produce/engine.rb +50 -0
  89. data/lib/textus/produce/render.rb +20 -0
  90. data/lib/textus/session.rb +15 -8
  91. data/lib/textus/store.rb +5 -14
  92. data/lib/textus/surfaces/mcp/catalog.rb +23 -11
  93. data/lib/textus/surfaces/mcp/errors.rb +0 -9
  94. data/lib/textus/surfaces/mcp/server.rb +87 -110
  95. data/lib/textus/surfaces/role_scope.rb +0 -4
  96. data/lib/textus/surfaces/watcher.rb +3 -3
  97. data/lib/textus/types.rb +15 -0
  98. data/lib/textus/version.rb +2 -2
  99. data/lib/textus/workflow/collector.rb +27 -0
  100. data/lib/textus/workflow/context.rb +5 -0
  101. data/lib/textus/workflow/dsl.rb +44 -0
  102. data/lib/textus/workflow/loader.rb +17 -0
  103. data/lib/textus/workflow/pattern.rb +18 -0
  104. data/lib/textus/workflow/registry.rb +21 -0
  105. data/lib/textus/workflow/runner.rb +99 -0
  106. data/lib/textus/workflow.rb +28 -0
  107. data/lib/textus.rb +12 -1
  108. metadata +67 -66
  109. data/lib/textus/action/pulse/scanner.rb +0 -118
  110. data/lib/textus/background/job/base.rb +0 -27
  111. data/lib/textus/background/job/materialize.rb +0 -31
  112. data/lib/textus/background/job/refresh.rb +0 -22
  113. data/lib/textus/background/job/sweep.rb +0 -31
  114. data/lib/textus/background/job.rb +0 -19
  115. data/lib/textus/background/planner/plan.rb +0 -113
  116. data/lib/textus/background/retention/apply.rb +0 -50
  117. data/lib/textus/doctor/check/handler_permit.rb +0 -34
  118. data/lib/textus/doctor/check/hooks.rb +0 -25
  119. data/lib/textus/doctor/check/intake_registration.rb +0 -46
  120. data/lib/textus/envelope/io/reader.rb +0 -48
  121. data/lib/textus/envelope/io/writer.rb +0 -211
  122. data/lib/textus/events.rb +0 -21
  123. data/lib/textus/init/templates/machine_intake.rb +0 -49
  124. data/lib/textus/init/templates/orientation_reducer.rb +0 -21
  125. data/lib/textus/manifest/entry/validators/events.rb +0 -22
  126. data/lib/textus/manifest/policy/handler_permit.rb +0 -17
  127. data/lib/textus/pipeline/acquire/handler.rb +0 -29
  128. data/lib/textus/pipeline/acquire/intake.rb +0 -132
  129. data/lib/textus/pipeline/acquire/projection.rb +0 -129
  130. data/lib/textus/pipeline/acquire/serializer/json.rb +0 -31
  131. data/lib/textus/pipeline/acquire/serializer/text.rb +0 -16
  132. data/lib/textus/pipeline/acquire/serializer/yaml.rb +0 -31
  133. data/lib/textus/pipeline/acquire/serializer.rb +0 -17
  134. data/lib/textus/pipeline/engine.rb +0 -97
  135. data/lib/textus/pipeline/render.rb +0 -25
  136. data/lib/textus/ports/audit_subscriber.rb +0 -42
  137. data/lib/textus/projection.rb +0 -103
  138. data/lib/textus/step/base.rb +0 -35
  139. data/lib/textus/step/builtin/csv_fetch.rb +0 -19
  140. data/lib/textus/step/builtin/ical_events_fetch.rb +0 -30
  141. data/lib/textus/step/builtin/json_fetch.rb +0 -18
  142. data/lib/textus/step/builtin/markdown_links_fetch.rb +0 -20
  143. data/lib/textus/step/builtin/rss_fetch.rb +0 -26
  144. data/lib/textus/step/builtin.rb +0 -22
  145. data/lib/textus/step/catalog.rb +0 -37
  146. data/lib/textus/step/context.rb +0 -74
  147. data/lib/textus/step/discovery.rb +0 -24
  148. data/lib/textus/step/error_log.rb +0 -32
  149. data/lib/textus/step/event_bus.rb +0 -113
  150. data/lib/textus/step/fetch.rb +0 -13
  151. data/lib/textus/step/fire_report.rb +0 -23
  152. data/lib/textus/step/loader.rb +0 -108
  153. data/lib/textus/step/observe.rb +0 -31
  154. data/lib/textus/step/registry_store.rb +0 -66
  155. data/lib/textus/step/signature.rb +0 -31
  156. data/lib/textus/step/transform.rb +0 -12
  157. data/lib/textus/step/validate.rb +0 -11
  158. data/lib/textus/step.rb +0 -10
  159. data/lib/textus/surfaces/mcp/session.rb +0 -9
  160. data/lib/textus/surfaces/mcp/tool_schemas.rb +0 -17
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # The single source of truth for step event names and their required
6
- # kwargs. EventBus, RegistryStore, and the Loader all read these
7
- # tables directly — the registries do not keep their own copies. Catalog
8
- # references no other constant, so it has no load-order cycle, which is
9
- # what removed the previous drift hazard (EventBus held a hard-coded
10
- # `RPC_EVENTS` list that could fall out of sync with RpcRegistry's table).
11
- module Catalog
12
- # Pub-sub events: 0..N handlers, fire-and-forget, receive `ctx:`.
13
- PUBSUB = {
14
- entry_written: %i[ctx key envelope],
15
- entry_deleted: %i[ctx key],
16
- entry_fetched: %i[ctx key envelope change],
17
- entry_renamed: %i[ctx key from_key to_key envelope],
18
- entry_produced: %i[ctx key envelope sources],
19
- produce_failed: %i[ctx keys error],
20
- proposal_accepted: %i[ctx key target_key],
21
- proposal_rejected: %i[ctx key target_key],
22
- entry_published: %i[ctx key envelope source target],
23
- store_loaded: %i[ctx],
24
- session_opened: %i[ctx role cursor],
25
- entry_fetch_started: %i[ctx key mode],
26
- entry_fetch_failed: %i[ctx key error_class error_message],
27
- }.freeze
28
-
29
- # RPC events: single handler, return value matters, receive `caps:`.
30
- RPC = {
31
- resolve_handler: %i[caps config args],
32
- transform_rows: %i[caps rows config],
33
- validate: %i[caps],
34
- }.freeze
35
- end
36
- end
37
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # A narrow handle passed to user steps in place of the raw Store.
6
- # All writes route back through the RoleScope so authorization, audit
7
- # logging, and schema validation always fire.
8
- class Context
9
- attr_reader :role, :correlation_id
10
-
11
- def self.for(container:, call:)
12
- scope = Textus::Surfaces::RoleScope.new(
13
- container: container,
14
- role: call.role,
15
- correlation_id: call.correlation_id,
16
- dry_run: call.dry_run,
17
- )
18
- new(scope: scope)
19
- end
20
-
21
- def initialize(scope:)
22
- @scope = scope
23
- @role = scope.role
24
- @correlation_id = scope.correlation_id
25
- end
26
-
27
- def backend
28
- @scope
29
- end
30
-
31
- # read — a pure-observation surface: nothing here ingests. Since ADR 0089
32
- # `get` itself is a pure read (the read-through that once forced this
33
- # surface to opt out is gone, so the old re-entrancy/deadlock guard is no
34
- # longer needed); `list`/`deps`/`freshness` are reads too. A hook observes
35
- # current state and never triggers an I/O cascade.
36
- def get(key) = pure_reader.call(key)
37
- def list(**) = @scope.list(**)
38
- def deps(key) = @scope.deps(key)
39
- def freshness(key) = @scope.freshness(key)
40
-
41
- # write (authorized + audited)
42
- def put(key, **) = @scope.put(key, **)
43
- def delete(key, **) = @scope.key_delete(key, **)
44
-
45
- def audit(verb, key:, **)
46
- @scope.container.audit_log.append(role: @role, verb: verb, key: key, **)
47
- end
48
-
49
- # fan-out
50
- def publish_followup(event, **)
51
- @scope.container.steps.publish(event, ctx: self, **)
52
- end
53
-
54
- def inspect
55
- "#<Textus::Step::Context role=#{@role} correlation_id=#{@correlation_id}>"
56
- end
57
-
58
- private
59
-
60
- def pure_reader
61
- @pure_reader ||= lambda do |key|
62
- Textus::Action::Get.new(key: key).call(
63
- container: @scope.container,
64
- call: Textus::Call.build(
65
- role: @scope.role,
66
- correlation_id: @scope.correlation_id,
67
- dry_run: @scope.dry_run?,
68
- ),
69
- )
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Maps a discovered file path under .textus/steps to its (kind, name).
6
- # kind = the directory segment directly under steps/; name = the basename
7
- # without .rb (hyphens preserved). The single source of truth for valid
8
- # kinds is the set of Base subclasses.
9
- KINDS = %i[fetch transform validate observe].freeze
10
-
11
- Discovery = Data.define(:kind, :name) do
12
- def self.parse(path, base:)
13
- rel = path.delete_prefix(base.to_s).delete_prefix("/")
14
- parts = rel.split("/")
15
- raise UsageError.new("step #{rel} must live under steps/<kind>/<name>.rb") unless parts.length == 2
16
-
17
- kind = parts[0].to_sym
18
- raise UsageError.new("unknown step kind '#{parts[0]}' (expected one of: #{KINDS.join(", ")})") unless KINDS.include?(kind)
19
-
20
- new(kind: kind, name: File.basename(parts[1], ".rb").to_sym)
21
- end
22
- end
23
- end
24
- end
@@ -1,32 +0,0 @@
1
- module Textus
2
- module Step
3
- # Bounded in-memory ring buffer of recent hook failures (errored and
4
- # timed_out). Each row carries the audit `seq` observed at the time of
5
- # failure so pulse can filter "errors since cursor".
6
- class ErrorLog
7
- DEFAULT_CAPACITY = 256
8
-
9
- def initialize(capacity: DEFAULT_CAPACITY)
10
- @capacity = capacity
11
- @rows = []
12
- @mutex = Mutex.new
13
- end
14
-
15
- def record(seq:, event:, hook:, key:, error_class:, error_message:)
16
- row = {
17
- seq: seq, event: event, hook: hook, key: key,
18
- error_class: error_class, error_message: error_message,
19
- at: Time.now.utc.iso8601
20
- }
21
- @mutex.synchronize do
22
- @rows << row
23
- @rows.shift while @rows.size > @capacity
24
- end
25
- end
26
-
27
- def since(seq)
28
- @mutex.synchronize { @rows.select { |r| r[:seq] > seq }.dup }
29
- end
30
- end
31
- end
32
- end
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "timeout"
4
-
5
- module Textus
6
- module Step
7
- class EventBus
8
- HOOK_TIMEOUT_SECONDS = 2
9
-
10
- class HookTimeout < StandardError; end
11
-
12
- def initialize(error_log: ErrorLog.new)
13
- @pubsub = Hash.new { |h, k| h[k] = [] }
14
- @error_handlers = []
15
- @error_log = error_log
16
- end
17
-
18
- attr_reader :error_log
19
-
20
- def on(event, name, keys: nil, &) = register(event, name, keys: keys, &)
21
-
22
- def register(event, name, keys: nil, &blk)
23
- event_sym = event.to_sym
24
- raise UsageError.new("#{event_sym} is an RPC event; register on RpcRegistry") if Catalog::RPC.key?(event_sym)
25
-
26
- required = Catalog::PUBSUB[event_sym] or raise UsageError.new("unknown event: #{event}")
27
- sig = Signature.new(blk)
28
- missing = sig.missing(required)
29
- if missing.any?
30
- raise UsageError.new("#{event_sym} hooks must accept kwargs: #{required.join(", ")} (missing: #{missing.join(", ")})")
31
- end
32
-
33
- name = name.to_sym
34
- raise UsageError.new("#{event_sym} hook '#{name}' already registered") if @pubsub[event_sym].any? { |h| h[:name] == name }
35
-
36
- @pubsub[event_sym] << { name: name, callable: blk, keys: keys }
37
- end
38
-
39
- def on_error(&block) = @error_handlers << block
40
-
41
- def listeners(event, key:) = @pubsub[event.to_sym].select { |h| match?(h[:keys], key) }
42
-
43
- def pubsub_handlers(event) = @pubsub[event.to_sym]
44
-
45
- def pubsub_handlers_names = @pubsub.values.flatten.map { |h| h[:name] }
46
-
47
- def publish(event, strict: false, **kwargs)
48
- key = kwargs[:key] || "-"
49
- fired = []
50
- errored = []
51
- timed_out = []
52
- raised = nil
53
-
54
- @pubsub[event.to_sym].each do |sub|
55
- next unless match?(sub[:keys], key)
56
-
57
- outcome, err = invoke(event, sub, key, kwargs)
58
- case outcome
59
- when :ok then fired << sub[:name]
60
- when :errored then errored << sub[:name]
61
- when :timed_out then timed_out << sub[:name]
62
- end
63
- raised ||= err if strict && err
64
- end
65
-
66
- raise raised if strict && raised
67
-
68
- FireReport.new(fired: fired, errored: errored, timed_out: timed_out)
69
- end
70
-
71
- private
72
-
73
- def invoke(event, sub, key, kwargs)
74
- accepted = Signature.new(sub[:callable]).filter(kwargs)
75
- begin
76
- Timeout.timeout(HOOK_TIMEOUT_SECONDS) do
77
- sub[:callable].call(**accepted)
78
- end
79
- [:ok, nil]
80
- rescue Timeout::Error
81
- err = HookTimeout.new("hook #{sub[:name]} exceeded #{HOOK_TIMEOUT_SECONDS}s on event #{event}")
82
- notify_error(event, sub, key, kwargs, err)
83
- [:timed_out, err]
84
- rescue StandardError => e
85
- notify_error(event, sub, key, kwargs, e)
86
- [:errored, e]
87
- end
88
- end
89
-
90
- def notify_error(event, sub, key, kwargs, error)
91
- @error_log.record(
92
- seq: kwargs[:_audit_seq] || -1,
93
- event: event,
94
- hook: sub[:name],
95
- key: key,
96
- error_class: error.class.name,
97
- error_message: error.message,
98
- )
99
- @error_handlers.each do |handler|
100
- handler.call(event: event, hook: sub[:name], key: key, kwargs: kwargs, error: error)
101
- rescue StandardError => e
102
- warn "[textus] error handler failed: #{e.class}: #{e.message}"
103
- end
104
- end
105
-
106
- def match?(globs, key)
107
- return true if globs.nil?
108
-
109
- Array(globs).any? { |g| File.fnmatch?(g, key.to_s, File::FNM_PATHNAME) }
110
- end
111
- end
112
- end
113
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Acquires data from outside the store (the `fetch:` build input). Returns
6
- # either { "content" => Hash } (structured) or { _meta:, body: } (rendered
7
- # text). Replaces the :resolve_handler RPC. `caps:` is injected by the
8
- # registry only if #call declares it.
9
- class Fetch < Base
10
- def self.required_kwargs = %i[config args]
11
- end
12
- end
13
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Outcome of a single Dispatcher#publish call.
6
- #
7
- # fired — hook names that ran to completion
8
- # errored — hook names that raised
9
- # timed_out — hook names whose worker thread exceeded the deadline
10
- #
11
- # Callers that care about hook health (tests, strict embedders) can
12
- # check #ok? or inspect #failures. The dispatcher itself never raises
13
- # on a hook failure unless strict: true was passed to #publish.
14
- FireReport = Data.define(:fired, :errored, :timed_out) do
15
- def initialize(fired:, errored:, timed_out:)
16
- super(fired: fired.dup.freeze, errored: errored.dup.freeze, timed_out: timed_out.dup.freeze)
17
- end
18
-
19
- def ok? = errored.empty? && timed_out.empty?
20
- def failures = errored + timed_out
21
- end
22
- end
23
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Convention discovery: glob .textus/steps/<kind>/<name>.rb, load each file,
6
- # validate the class it defines against the discovered kind, assign the
7
- # discovered name, and register it. No global queue, no Textus.hook.
8
- class Loader
9
- BASE_FOR = {
10
- fetch: Step::Fetch, transform: Step::Transform,
11
- validate: Step::Validate, observe: Step::Observe
12
- }.freeze
13
-
14
- def initialize(registry:)
15
- @registry = registry
16
- end
17
-
18
- def load_dir(dir)
19
- return unless File.directory?(dir)
20
-
21
- loaded_paths = Set.new
22
- Dir.glob(File.join(dir, "**/*.rb")).sort.each do |path| # rubocop:disable Lint/RedundantDirGlobSort
23
- real_path = File.realpath(path)
24
- next if loaded_paths.include?(real_path)
25
-
26
- loaded_paths << real_path
27
- load_one(dir, path)
28
- end
29
- end
30
-
31
- private
32
-
33
- def load_one(dir, path)
34
- disc = Discovery.parse(path, base: dir)
35
- klass = capture_defined_class(path, disc)
36
- validate!(disc, klass, path, dir)
37
-
38
- step = klass.new
39
- step.name = disc.name
40
- @registry.register(step)
41
- rescue StandardError, ScriptError => e
42
- raise UsageError.new("failed loading step #{rel(dir, path)}: #{e.class}: #{e.message}") unless e.is_a?(UsageError)
43
-
44
- raise
45
- end
46
-
47
- # Load the file and return the Step::Base subclass it newly defined.
48
- def capture_defined_class(path, disc)
49
- before = descendants
50
- load(path)
51
- defined = descendants - before
52
- return defined.first if defined.length == 1
53
- raise UsageError.new("step #{path} defined more than one Textus::Step subclass") if defined.length > 1
54
-
55
- fallback = find_existing_class_for(disc)
56
- return fallback if fallback
57
-
58
- raise UsageError.new("step #{path} defined no Textus::Step subclass")
59
- end
60
-
61
- def find_existing_class_for(disc)
62
- expected = inferred_class_name_for(disc)
63
- candidates = descendants.select do |klass|
64
- klass < BASE_FOR.fetch(disc.kind) && klass.name&.split("::")&.last == expected
65
- end
66
- return nil if candidates.empty?
67
- return candidates.first if candidates.length == 1
68
-
69
- namespaced = candidates.find { |klass| klass.name == "Textus::Step::#{expected}" }
70
- namespaced || candidates.first
71
- end
72
-
73
- def inferred_class_name_for(disc)
74
- stem = disc.name.to_s.split(/[-_]/).map(&:capitalize).join
75
- suffix = { fetch: "Fetch", transform: "Transform", validate: "Validate", observe: "Observe" }.fetch(disc.kind)
76
- "#{stem}#{suffix}"
77
- end
78
-
79
- def validate!(disc, klass, path, dir)
80
- expected = BASE_FOR.fetch(disc.kind)
81
- actual_kind = klass.respond_to?(:kind) ? safe_kind(klass) : nil
82
- unless klass < expected
83
- raise UsageError.new("#{rel(dir, path)} defines a #{actual_kind || "non-step"} step but lives under #{disc.kind}/")
84
- end
85
-
86
- sig = Step::Signature.new(klass.instance_method(:call))
87
- missing = sig.missing(klass.required_kwargs)
88
- return if missing.empty?
89
-
90
- msg = "#{disc.kind} step '#{disc.name}' #call must accept kwargs: " \
91
- "#{klass.required_kwargs.join(", ")} (missing: #{missing.join(", ")})"
92
- raise UsageError.new(msg)
93
- end
94
-
95
- def safe_kind(klass)
96
- klass.kind
97
- rescue StandardError
98
- nil
99
- end
100
-
101
- def descendants
102
- ObjectSpace.each_object(Class).select { |c| c < Step::Base }
103
- end
104
-
105
- def rel(dir, path) = path.delete_prefix(dir.to_s).delete_prefix("/")
106
- end
107
- end
108
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Reacts to a lifecycle event (Catalog::PUBSUB). 0..N per event,
6
- # fire-and-forget, no meaningful return, timeout-isolated by the EventBus.
7
- # Declares its event with `on :event_name` and an optional key filter with
8
- # `match "glob.**"`. Replaces user pub/sub subscribers.
9
- class Observe < Base
10
- def self.on(event = :__read__)
11
- if event == :__read__
12
- @event
13
- else
14
- @event = event.to_sym
15
- end
16
- end
17
-
18
- def self.match(glob = :__read__)
19
- if glob == :__read__
20
- @match
21
- else
22
- @match = glob
23
- end
24
- end
25
-
26
- class << self
27
- attr_reader :event
28
- end
29
- end
30
- end
31
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # The single home for registered steps. Reuses Step::EventBus for observe
6
- # (pub/sub) dispatch — including its timeout isolation and error ring buffer
7
- # — and holds a kind->{name->instance} table for the invocable kinds
8
- # (fetch/transform/validate). Replaces the EventBus+RpcRegistryStore pair.
9
- class RegistryStore
10
- def initialize(error_log: Step::ErrorLog.new)
11
- @bus = Step::EventBus.new(error_log: error_log)
12
- @table = Hash.new { |h, k| h[k] = {} }
13
- end
14
-
15
- # Register either a Step instance (invocable/observe table) or a pub/sub
16
- # subscriber (`register(event, name, keys:, &block)`) for internal/spec
17
- # probes.
18
- def register(step, *, keys: nil, &)
19
- return @bus.register(step, *, keys: keys, &) unless step.is_a?(Step::Base)
20
-
21
- kind = step.class.kind
22
- name = step.name.to_sym
23
- return register_observe(step) if kind == :observe
24
-
25
- raise UsageError.new("#{kind} '#{name}' already registered") if @table[kind].key?(name)
26
-
27
- @table[kind][name] = step
28
- end
29
-
30
- # Invoke an invocable step. Mirrors the old RpcRegistryStore#invoke: inject
31
- # caps only when #call declares :caps or accepts keyrest.
32
- def invoke(kind, name, caps:, **other)
33
- step = @table[kind][name.to_sym] or raise UsageError.new("unknown #{kind}: #{name}")
34
- sig = Step::Signature.new(step.method(:call))
35
- kwargs = other.dup
36
- kwargs[:caps] = caps if sig.accepts_keyrest? || sig.declared_keys.include?(:caps)
37
- step.call(**kwargs)
38
- end
39
-
40
- def names(kind)
41
- return @bus.pubsub_handlers_names if kind.to_sym == :observe
42
-
43
- @table[kind.to_sym].keys
44
- end
45
-
46
- def pubsub_handlers(event)
47
- @bus.pubsub_handlers(event)
48
- end
49
-
50
- # Pub/sub passthrough (observe + internal built-in subscribers).
51
- def publish(event, **) = @bus.publish(event, **)
52
- def on(event, name, keys: nil, &) = @bus.register(event, name, keys: keys, &)
53
- def on_error(&) = @bus.on_error(&)
54
- def error_log = @bus.error_log
55
-
56
- private
57
-
58
- def register_observe(step)
59
- sig = Step::Signature.new(step.method(:call))
60
- @bus.register(step.class.event, step.name, keys: step.class.match) do |**kw|
61
- step.call(**sig.filter(kw))
62
- end
63
- end
64
- end
65
- end
66
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- class Signature
6
- def initialize(callable)
7
- @params = callable.parameters
8
- end
9
-
10
- def accepts_keyrest?
11
- @params.any? { |type, _| type == :keyrest }
12
- end
13
-
14
- def declared_keys
15
- @params.each_with_object([]) { |(t, n), acc| acc << n if %i[keyreq key].include?(t) }
16
- end
17
-
18
- def missing(required)
19
- return [] if accepts_keyrest?
20
-
21
- required - declared_keys
22
- end
23
-
24
- def filter(kwargs)
25
- return kwargs if accepts_keyrest?
26
-
27
- kwargs.slice(*declared_keys)
28
- end
29
- end
30
- end
31
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Combines/reshapes projected rows into an artifact shape. Returns a Hash
6
- # (the structured payload base) or an Array of rows. Replaces the
7
- # :transform_rows RPC. (Phase 2 will widen `rows:` to a named `inputs:` map.)
8
- class Transform < Base
9
- def self.required_kwargs = %i[rows config]
10
- end
11
- end
12
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Step
5
- # Checks an artifact/store state and returns diagnostics. Replaces the
6
- # :validate RPC. Receives only `caps:` (injected by the registry).
7
- class Validate < Base
8
- def self.required_kwargs = []
9
- end
10
- end
11
- end
data/lib/textus/step.rb DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- # A Step is a unit of user-extensible behaviour discovered by convention at
5
- # .textus/steps/<kind>/<name>.rb. Five kinds: fetch (external acquisition),
6
- # transform (combine/reshape into an artifact), validate (check an artifact),
7
- # observe (react to a lifecycle event). Replaces the Textus.hook block queue.
8
- module Step
9
- end
10
- end
@@ -1,9 +0,0 @@
1
- module Textus
2
- module Surfaces
3
- module MCP
4
- # The session value now lives in core (ADR 0036); retained here as an
5
- # alias so existing MCP references keep resolving.
6
- Session = Textus::Session
7
- end
8
- end
9
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Surfaces
3
- module MCP
4
- # Kept for name stability (ADR 0039). The JSON schemas are DERIVED from
5
- # per-verb contracts; this delegates to MCP::Catalog. The hand-written
6
- # array is gone — a kwarg rename now updates the schema automatically (and
7
- # the signature guard fails if the contract lags the use-case).
8
- module ToolSchemas
9
- module_function
10
-
11
- def all
12
- Catalog.tool_schemas
13
- end
14
- end
15
- end
16
- end
17
- end