textus 0.26.0 → 0.29.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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +111 -67
  3. data/CHANGELOG.md +76 -0
  4. data/README.md +55 -13
  5. data/SPEC.md +75 -38
  6. data/docs/conventions.md +4 -4
  7. data/lib/textus/boot.rb +14 -10
  8. data/lib/textus/builder/pipeline.rb +13 -12
  9. data/lib/textus/call.rb +28 -0
  10. data/lib/textus/cli/verb/audit.rb +1 -1
  11. data/lib/textus/cli/verb/boot.rb +1 -1
  12. data/lib/textus/cli/verb/build.rb +2 -2
  13. data/lib/textus/cli/verb/doctor.rb +1 -1
  14. data/lib/textus/cli/verb/hook_run.rb +2 -2
  15. data/lib/textus/cli/verb/put.rb +3 -3
  16. data/lib/textus/cli/verb.rb +6 -6
  17. data/lib/textus/cli.rb +0 -7
  18. data/lib/textus/container.rb +23 -0
  19. data/lib/textus/dispatcher.rb +49 -0
  20. data/lib/textus/doctor/check/audit_log.rb +1 -1
  21. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  22. data/lib/textus/doctor/check/sentinels.rb +10 -8
  23. data/lib/textus/doctor/check.rb +12 -5
  24. data/lib/textus/doctor.rb +7 -7
  25. data/lib/textus/domain/authorizer.rb +2 -2
  26. data/lib/textus/domain/sentinel.rb +9 -65
  27. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  28. data/lib/textus/domain/staleness/intake_check.rb +18 -10
  29. data/lib/textus/domain/staleness.rb +3 -3
  30. data/lib/textus/{application/envelope → envelope/io}/reader.rb +2 -2
  31. data/lib/textus/{application/envelope → envelope/io}/writer.rb +11 -11
  32. data/lib/textus/hooks/context.rb +30 -13
  33. data/lib/textus/hooks/rpc_registry.rb +1 -1
  34. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  35. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  36. data/lib/textus/maintenance/migrate.rb +51 -0
  37. data/lib/textus/maintenance/rule_lint.rb +56 -0
  38. data/lib/textus/maintenance/zone_mv.rb +51 -0
  39. data/lib/textus/maintenance.rb +15 -0
  40. data/lib/textus/manifest/data.rb +4 -3
  41. data/lib/textus/manifest/entry/base.rb +38 -18
  42. data/lib/textus/manifest/entry/derived.rb +6 -6
  43. data/lib/textus/manifest/entry/nested.rb +7 -9
  44. data/lib/textus/manifest/entry/parser.rb +2 -2
  45. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  46. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  47. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  48. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  49. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  50. data/lib/textus/manifest/entry/validators.rb +2 -2
  51. data/lib/textus/manifest/entry.rb +0 -5
  52. data/lib/textus/manifest.rb +1 -6
  53. data/lib/textus/mcp/server.rb +1 -2
  54. data/lib/textus/mcp/session.rb +10 -1
  55. data/lib/textus/mcp/tools.rb +2 -2
  56. data/lib/textus/mcp.rb +1 -1
  57. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  58. data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
  59. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  60. data/lib/textus/{infra → ports}/clock.rb +1 -1
  61. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  62. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  63. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  64. data/lib/textus/ports/sentinel_store.rb +67 -0
  65. data/lib/textus/ports/storage/file_stat.rb +19 -0
  66. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  67. data/lib/textus/projection.rb +91 -0
  68. data/lib/textus/read/audit.rb +111 -0
  69. data/lib/textus/read/blame.rb +81 -0
  70. data/lib/textus/read/boot.rb +18 -0
  71. data/lib/textus/read/deps.rb +24 -0
  72. data/lib/textus/read/doctor.rb +19 -0
  73. data/lib/textus/read/freshness.rb +101 -0
  74. data/lib/textus/read/get.rb +66 -0
  75. data/lib/textus/read/get_or_refresh.rb +69 -0
  76. data/lib/textus/read/list.rb +15 -0
  77. data/lib/textus/read/policy_explain.rb +37 -0
  78. data/lib/textus/read/published.rb +15 -0
  79. data/lib/textus/read/pulse.rb +89 -0
  80. data/lib/textus/read/rdeps.rb +25 -0
  81. data/lib/textus/read/schema_envelope.rb +16 -0
  82. data/lib/textus/read/stale.rb +17 -0
  83. data/lib/textus/read/uid.rb +20 -0
  84. data/lib/textus/read/validate_all.rb +22 -0
  85. data/lib/textus/read/validator.rb +84 -0
  86. data/lib/textus/read/where.rb +16 -0
  87. data/lib/textus/role_scope.rb +49 -0
  88. data/lib/textus/schema/tools.rb +3 -3
  89. data/lib/textus/store.rb +16 -7
  90. data/lib/textus/version.rb +1 -1
  91. data/lib/textus/write/accept.rb +86 -0
  92. data/lib/textus/write/authority_gate.rb +24 -0
  93. data/lib/textus/write/delete.rb +54 -0
  94. data/lib/textus/write/materializer.rb +48 -0
  95. data/lib/textus/write/mv.rb +123 -0
  96. data/lib/textus/write/publish.rb +66 -0
  97. data/lib/textus/write/put.rb +59 -0
  98. data/lib/textus/write/refresh_all.rb +44 -0
  99. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  100. data/lib/textus/write/refresh_worker.rb +138 -0
  101. data/lib/textus/write/reject.rb +54 -0
  102. data/lib/textus.rb +1 -2
  103. metadata +54 -50
  104. data/lib/textus/application/caps.rb +0 -49
  105. data/lib/textus/application/context.rb +0 -34
  106. data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
  107. data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
  108. data/lib/textus/application/maintenance/migrate.rb +0 -59
  109. data/lib/textus/application/maintenance/rule_lint.rb +0 -65
  110. data/lib/textus/application/maintenance/zone_mv.rb +0 -60
  111. data/lib/textus/application/maintenance.rb +0 -17
  112. data/lib/textus/application/projection.rb +0 -93
  113. data/lib/textus/application/read/audit.rb +0 -106
  114. data/lib/textus/application/read/blame.rb +0 -91
  115. data/lib/textus/application/read/deps.rb +0 -34
  116. data/lib/textus/application/read/freshness.rb +0 -110
  117. data/lib/textus/application/read/get.rb +0 -75
  118. data/lib/textus/application/read/get_or_refresh.rb +0 -63
  119. data/lib/textus/application/read/list.rb +0 -25
  120. data/lib/textus/application/read/policy_explain.rb +0 -47
  121. data/lib/textus/application/read/published.rb +0 -25
  122. data/lib/textus/application/read/pulse.rb +0 -101
  123. data/lib/textus/application/read/rdeps.rb +0 -35
  124. data/lib/textus/application/read/schema_envelope.rb +0 -26
  125. data/lib/textus/application/read/stale.rb +0 -23
  126. data/lib/textus/application/read/uid.rb +0 -30
  127. data/lib/textus/application/read/validate_all.rb +0 -32
  128. data/lib/textus/application/read/validator.rb +0 -86
  129. data/lib/textus/application/read/where.rb +0 -26
  130. data/lib/textus/application/use_case.rb +0 -22
  131. data/lib/textus/application/write/accept.rb +0 -102
  132. data/lib/textus/application/write/authority_gate.rb +0 -26
  133. data/lib/textus/application/write/delete.rb +0 -45
  134. data/lib/textus/application/write/materializer.rb +0 -49
  135. data/lib/textus/application/write/mv.rb +0 -118
  136. data/lib/textus/application/write/publish.rb +0 -96
  137. data/lib/textus/application/write/put.rb +0 -49
  138. data/lib/textus/application/write/refresh_all.rb +0 -63
  139. data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
  140. data/lib/textus/application/write/refresh_worker.rb +0 -134
  141. data/lib/textus/application/write/reject.rb +0 -62
  142. data/lib/textus/session.rb +0 -84
@@ -1,96 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- # Single-pass publish use case: dispatches polymorphically to each
5
- # entry's `publish_via` method. Derived entries materialize their body
6
- # via Materializer; Nested entries fan out via publish_each; Leaf and
7
- # Intake entries copy their stored body to publish_to targets. The
8
- # Publish layer owns wiring (context, accumulation) but not per-kind
9
- # logic.
10
- #
11
- # Return shape: { "protocol", "built", "published_leaves" }
12
- module Publish
13
- def self.call(*, session:, ctx:, caps:, **)
14
- Impl.new(
15
- ctx: ctx, caps: caps,
16
- rpc: session.rpc,
17
- session: session,
18
- hook_context: session.hook_context
19
- ).call(*, **)
20
- end
21
-
22
- class Impl
23
- def initialize(ctx:, caps:, rpc:, session:, hook_context:)
24
- @ctx = ctx
25
- @caps = caps
26
- @manifest = caps.manifest
27
- @file_store = caps.file_store
28
- @events = caps.events
29
- @root = caps.root
30
- @rpc = rpc
31
- @session = session
32
- @hook_context = hook_context
33
- end
34
-
35
- def call(prefix: nil)
36
- built = []
37
- leaves = []
38
- context = build_context
39
-
40
- @manifest.data.entries.each do |mentry|
41
- next if prefix && !entry_matches_prefix?(mentry, prefix)
42
-
43
- result = mentry.publish_via(context, prefix: prefix)
44
- next if result.nil?
45
-
46
- case result[:kind]
47
- when :built then built << result[:value]
48
- when :leaves then leaves.concat(result[:value])
49
- end
50
- end
51
-
52
- { "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => leaves }
53
- end
54
-
55
- private
56
-
57
- def build_context
58
- Textus::Manifest::Entry::Base::PublishContext.new(
59
- repo_root: File.dirname(@root),
60
- manifest: @manifest,
61
- file_store: @file_store,
62
- root: @root,
63
- caps: @caps,
64
- rpc: @rpc,
65
- session: @session,
66
- ctx: @ctx,
67
- bus: @events,
68
- hook_context: @hook_context,
69
- reader: reader,
70
- emit: ->(event, **payload) { @events.publish(event, ctx: @hook_context, **payload) },
71
- )
72
- end
73
-
74
- # Whether the entry should be processed for the given prefix filter.
75
- def entry_matches_prefix?(mentry, prefix)
76
- return true unless prefix
77
-
78
- case mentry
79
- when Textus::Manifest::Entry::Nested
80
- mentry.key.start_with?(prefix) ||
81
- prefix.start_with?("#{mentry.key}.")
82
- else
83
- mentry.key.start_with?(prefix)
84
- end
85
- end
86
-
87
- def reader
88
- @reader ||= Textus::Application::Read::Get::Impl.new(ctx: @ctx, caps: @caps)
89
- end
90
- end
91
- end
92
- end
93
- end
94
- end
95
-
96
- Textus::Application::UseCase.register(:publish, Textus::Application::Write::Publish, caps: :write)
@@ -1,49 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- module Put
5
- def self.call(*, session:, ctx:, caps:, **)
6
- Impl.new(
7
- ctx: ctx, caps: caps,
8
- writer: session.envelope_writer,
9
- hook_context: session.hook_context
10
- ).call(*, **)
11
- end
12
-
13
- class Impl
14
- def initialize(ctx:, caps:, writer:, hook_context:)
15
- @ctx = ctx
16
- @manifest = caps.manifest
17
- @events = caps.events
18
- @authorizer = caps.authorizer
19
- @writer = writer
20
- @hook_context = hook_context
21
- end
22
-
23
- def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
24
- Textus::Manifest::Data.validate_key!(key)
25
- mentry = @manifest.resolver.resolve(key).entry
26
-
27
- @authorizer.authorize_write!(mentry, role: @ctx.role)
28
-
29
- envelope = @writer.put(
30
- key,
31
- mentry: mentry,
32
- payload: Textus::Application::Envelope::Writer::Payload.new(meta: meta, body: body, content: content),
33
- if_etag: if_etag,
34
- )
35
-
36
- @events.publish(:entry_put,
37
- ctx: @hook_context,
38
- key: key,
39
- envelope: envelope)
40
-
41
- envelope
42
- end
43
- end
44
- end
45
- end
46
- end
47
- end
48
-
49
- Textus::Application::UseCase.register(:put, Textus::Application::Write::Put, caps: :write)
@@ -1,63 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- module RefreshAll
5
- def self.call(*, session:, ctx:, caps:, **)
6
- Impl.new(
7
- ctx: ctx, caps: caps,
8
- rpc: session.rpc,
9
- writer: session.envelope_writer,
10
- hook_context: session.hook_context
11
- ).call(*, **)
12
- end
13
-
14
- class Impl
15
- def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
16
- @ctx = ctx
17
- @caps = caps
18
- @rpc = rpc
19
- @writer = writer
20
- @hook_context = hook_context
21
- end
22
-
23
- def call(prefix: nil, zone: nil)
24
- worker = Textus::Application::Write::RefreshWorker::Impl.new(
25
- ctx: @ctx, caps: @caps, rpc: @rpc, writer: @writer,
26
- hook_context: @hook_context
27
- )
28
-
29
- stale_rows = Textus::Application::Read::Stale::Impl.new(caps: @caps).call(prefix: prefix, zone: zone)
30
- refreshed = []
31
- failed = []
32
- skipped = []
33
-
34
- stale_rows.each do |row|
35
- key = row["key"] || row[:key]
36
- reason = row["reason"] || row[:reason]
37
- if reason.to_s.match?(/ttl exceeded|never refreshed/)
38
- begin
39
- worker.run(key)
40
- refreshed << key
41
- rescue Textus::Error => e
42
- failed << { "key" => key, "error" => e.message }
43
- end
44
- else
45
- skipped << { "key" => key, "reason" => reason }
46
- end
47
- end
48
-
49
- {
50
- "protocol" => Textus::PROTOCOL,
51
- "ok" => failed.empty?,
52
- "refreshed" => refreshed,
53
- "failed" => failed,
54
- "skipped" => skipped,
55
- }
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
62
-
63
- Textus::Application::UseCase.register(:refresh_all, Textus::Application::Write::RefreshAll, caps: :write)
@@ -1,102 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- class RefreshOrchestrator
5
- def initialize(worker:, store_root:, events:, ctx: nil, hook_context: nil, detached_spawner: nil)
6
- @worker = worker
7
- @store_root = store_root
8
- @events = events
9
- @ctx = ctx
10
- @hook_context = hook_context
11
- @detached_spawner = detached_spawner || default_spawner
12
- end
13
-
14
- def execute(action, key:)
15
- case action
16
- when Textus::Domain::Action::Return then Textus::Domain::Outcome::Skipped.new
17
- when Textus::Domain::Action::RefreshSync then run_sync(key)
18
- when Textus::Domain::Action::RefreshTimed then run_timed(action.budget_ms, key)
19
- else raise ArgumentError.new("unknown action: #{action.inspect}")
20
- end
21
- end
22
-
23
- private
24
-
25
- def run_sync(key)
26
- envelope = @worker.run(key)
27
- Textus::Domain::Outcome::Refreshed.new(envelope: envelope)
28
- rescue Textus::Error => e
29
- Textus::Domain::Outcome::Failed.new(error: e)
30
- end
31
-
32
- def run_timed(budget_ms, key)
33
- return run_timed_with_fork(budget_ms, key) if Textus::Infra::Refresh::Detached.supported?
34
-
35
- run_timed_cooperative(budget_ms, key)
36
- end
37
-
38
- def run_timed_cooperative(budget_ms, key)
39
- result = nil
40
- thread = Thread.new do
41
- result = @worker.run(key)
42
- rescue Textus::Error => e
43
- result = e
44
- end
45
-
46
- thread.join(budget_ms / 1000.0)
47
- if thread.alive?
48
- thread.kill
49
- return Textus::Domain::Outcome::Failed.new(
50
- error: Textus::UsageError.new(
51
- "refresh exceeded budget #{budget_ms}ms (no fork available — cooperative cancel)",
52
- ),
53
- )
54
- end
55
-
56
- if result.is_a?(Textus::Error)
57
- Textus::Domain::Outcome::Failed.new(error: result)
58
- else
59
- Textus::Domain::Outcome::Refreshed.new(envelope: result)
60
- end
61
- end
62
-
63
- def run_timed_with_fork(budget_ms, key)
64
- result = nil
65
- thread = Thread.new do
66
- result = @worker.run(key)
67
- rescue Textus::Error => e
68
- result = e
69
- end
70
-
71
- thread.join(budget_ms / 1000.0)
72
-
73
- if thread.alive?
74
- thread.kill
75
-
76
- # Single-flight: if a sibling process / earlier fork holds the
77
- # per-leaf lock, don't fork another worker — they're already
78
- # doing this work.
79
- probe = Textus::Infra::Refresh::Lock.new(root: @store_root, key: key)
80
- return Textus::Domain::Outcome::Detached.new unless probe.try_acquire
81
-
82
- probe.release
83
-
84
- payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
85
- payload[:ctx] = @hook_context if @hook_context
86
- @events.publish(:refresh_backgrounded, **payload)
87
- @detached_spawner.call(store_root: @store_root, key: key)
88
- Textus::Domain::Outcome::Detached.new
89
- elsif result.is_a?(Textus::Error)
90
- Textus::Domain::Outcome::Failed.new(error: result)
91
- else
92
- Textus::Domain::Outcome::Refreshed.new(envelope: result)
93
- end
94
- end
95
-
96
- def default_spawner
97
- Textus::Infra::Refresh::Detached.method(:spawn)
98
- end
99
- end
100
- end
101
- end
102
- end
@@ -1,134 +0,0 @@
1
- require "timeout"
2
-
3
- module Textus
4
- module Application
5
- module Write
6
- module RefreshWorker
7
- FETCH_TIMEOUT_SECONDS = 30
8
-
9
- def self.call(*, session:, ctx:, caps:, **)
10
- Impl.new(
11
- ctx: ctx, caps: caps,
12
- rpc: session.rpc,
13
- writer: session.envelope_writer,
14
- hook_context: session.hook_context
15
- ).call(*, **)
16
- end
17
-
18
- class Impl
19
- def initialize(ctx:, caps:, rpc:, writer:, hook_context:)
20
- @ctx = ctx
21
- @caps = caps
22
- @manifest = caps.manifest
23
- @writer = writer
24
- @events = caps.events
25
- @rpc = rpc
26
- @authorizer = caps.authorizer
27
- @hook_context = hook_context
28
- end
29
-
30
- # call(key) is the primary entry; run is kept as an alias for
31
- # Orchestrator and RefreshAll which call worker.run(key).
32
- def call(key)
33
- run(key)
34
- end
35
-
36
- def run(key)
37
- res = @manifest.resolver.resolve(key)
38
- mentry = res.entry
39
- path = res.path
40
- remaining = res.remaining
41
- raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
42
-
43
- before_etag = File.exist?(path) ? Etag.for_file(path) : nil
44
- result = fetch_with_events(key, mentry, remaining)
45
- persist_and_notify(key, mentry, result, before_etag)
46
- end
47
-
48
- private
49
-
50
- def fetch_timeout_for(key)
51
- rule = @manifest.rules.for(key)
52
- rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
53
- end
54
-
55
- def fetch_with_events(key, mentry, remaining)
56
- @events.publish(:refresh_started, ctx: @hook_context, key: key, mode: :sync)
57
- call_intake(key, mentry, remaining)
58
- end
59
-
60
- def call_intake(key, mentry, remaining)
61
- timeout = fetch_timeout_for(key)
62
- Timeout.timeout(timeout) do
63
- @rpc.invoke(:resolve_intake, mentry.handler,
64
- caps: @caps,
65
- config: mentry.config,
66
- args: { trigger_key: key, leaf_segments: remaining || [] })
67
- end
68
- rescue Timeout::Error
69
- @events.publish(:refresh_failed, ctx: @hook_context, key: key,
70
- error_class: "Timeout::Error",
71
- error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
72
- raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
73
- rescue Textus::Error => e
74
- @events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
75
- error_message: e.message)
76
- raise
77
- rescue StandardError => e
78
- @events.publish(:refresh_failed, ctx: @hook_context, key: key, error_class: e.class.name,
79
- error_message: e.message)
80
- raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
81
- end
82
-
83
- def persist_and_notify(key, mentry, result, before_etag)
84
- normalized = RefreshWorker.send(:normalize_action_result, result, format: mentry.format)
85
- @authorizer.authorize_write!(mentry, role: @ctx.role)
86
- envelope = @writer.put(
87
- key,
88
- mentry: mentry,
89
- payload: Textus::Application::Envelope::Writer::Payload.new(
90
- meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
91
- ),
92
- )
93
- change = detect_change(before_etag, envelope)
94
- @events.publish(:entry_refreshed, ctx: @hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
95
- envelope
96
- end
97
-
98
- def detect_change(before_etag, envelope)
99
- if before_etag.nil? then :created
100
- elsif envelope.etag == before_etag then :unchanged
101
- else :updated
102
- end
103
- end
104
- end
105
-
106
- def self.normalize_action_result(res, format:)
107
- res = res.transform_keys(&:to_s) if res.is_a?(Hash)
108
- res ||= {}
109
- meta_val = res["_meta"]
110
- body = res["body"]
111
- content = res["content"]
112
-
113
- case format
114
- when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
115
- when "text" then { meta: {}, body: body.to_s, content: nil }
116
- when "json", "yaml"
117
- if !content.nil?
118
- { meta: meta_val || {}, body: nil, content: content }
119
- elsif !body.nil?
120
- { meta: {}, body: body.to_s, content: nil }
121
- else
122
- raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
123
- end
124
- else
125
- raise Textus::UsageError.new("unknown format #{format.inspect}")
126
- end
127
- end
128
- private_class_method :normalize_action_result
129
- end
130
- end
131
- end
132
- end
133
-
134
- Textus::Application::UseCase.register(:refresh, Textus::Application::Write::RefreshWorker, caps: :write)
@@ -1,62 +0,0 @@
1
- require_relative "authority_gate"
2
-
3
- module Textus
4
- module Application
5
- module Write
6
- module Reject
7
- def self.call(*, session:, ctx:, caps:, **)
8
- Impl.new(
9
- ctx: ctx, caps: caps,
10
- writer: session.envelope_writer,
11
- hook_context: session.hook_context
12
- ).call(*, **)
13
- end
14
-
15
- class Impl
16
- include AuthorityGate
17
-
18
- def initialize(ctx:, caps:, writer:, hook_context:)
19
- @ctx = ctx
20
- @caps = caps
21
- @manifest = caps.manifest
22
- @writer = writer
23
- @events = caps.events
24
- @authorizer = caps.authorizer
25
- @hook_context = hook_context
26
- end
27
-
28
- def call(pending_key)
29
- assert_accept_authority!("reject")
30
-
31
- mentry = @manifest.resolver.resolve(pending_key).entry
32
- unless mentry.in_proposal_zone?
33
- raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
34
- end
35
-
36
- env = Textus::Application::Read::Get::Impl.new(
37
- ctx: @ctx, caps: @caps,
38
- ).call(pending_key)
39
- proposal = env.meta&.dig("proposal") or
40
- raise ProposalError.new("entry has no proposal block: #{pending_key}")
41
- target_key = proposal["target_key"] or
42
- raise ProposalError.new("proposal missing target_key")
43
-
44
- Textus::Application::Write::Delete::Impl.new(
45
- ctx: @ctx, caps: @caps, writer: @writer,
46
- hook_context: @hook_context
47
- ).call(pending_key, suppress_events: true)
48
-
49
- @events.publish(:proposal_rejected,
50
- ctx: @hook_context,
51
- key: pending_key,
52
- target_key: target_key)
53
-
54
- { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
61
-
62
- Textus::Application::UseCase.register(:reject, Textus::Application::Write::Reject, caps: :write)
@@ -1,84 +0,0 @@
1
- module Textus
2
- # Per-call session. Holds ctx (role, correlation_id, now, dry_run) and
3
- # the three caps records. Generates one method per registered use case.
4
- class Session
5
- attr_reader :ctx, :read_caps, :write_caps, :hook_caps
6
-
7
- def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
8
- read_caps, write_caps, hook_caps = Application.caps_from_store(store)
9
- new(
10
- ctx: Application::Context.build(role: role, correlation_id: correlation_id, dry_run: dry_run),
11
- read_caps: read_caps, write_caps: write_caps, hook_caps: hook_caps
12
- )
13
- end
14
-
15
- def initialize(ctx:, read_caps:, write_caps:, hook_caps:)
16
- @ctx = ctx
17
- @read_caps = read_caps
18
- @write_caps = write_caps
19
- @hook_caps = hook_caps
20
- end
21
-
22
- def with_role(role)
23
- self.class.new(
24
- ctx: @ctx.with_role(role),
25
- read_caps: @read_caps, write_caps: @write_caps, hook_caps: @hook_caps
26
- )
27
- end
28
-
29
- def hook_context
30
- @hook_context ||= Hooks::Context.new(session: self)
31
- end
32
-
33
- def rpc = @hook_caps.rpc
34
- def events = @hook_caps.events
35
-
36
- def envelope_reader
37
- @envelope_reader ||= Application::Envelope::Reader.new(
38
- file_store: @read_caps.file_store, manifest: @read_caps.manifest,
39
- )
40
- end
41
-
42
- def envelope_writer
43
- @envelope_writer ||= Application::Envelope::Writer.new(
44
- file_store: @write_caps.file_store, manifest: @write_caps.manifest,
45
- schemas: @write_caps.schemas, audit_log: @write_caps.audit_log,
46
- ctx: @ctx, reader: envelope_reader
47
- )
48
- end
49
-
50
- def boot(...) = Textus::Boot.run(self, ...)
51
- def doctor(...) = Textus::Doctor.run(self, ...)
52
-
53
- def refresh_orchestrator
54
- @refresh_orchestrator ||= Application::Write::RefreshOrchestrator.new(
55
- worker: refresh_worker,
56
- store_root: @write_caps.root,
57
- events: @write_caps.events,
58
- ctx: @ctx,
59
- hook_context: hook_context,
60
- )
61
- end
62
-
63
- def refresh_worker
64
- @refresh_worker ||= Application::Write::RefreshWorker::Impl.new(
65
- ctx: @ctx, caps: @write_caps,
66
- rpc: rpc, writer: envelope_writer, hook_context: hook_context
67
- )
68
- end
69
-
70
- # Generated dispatch methods. Defined AFTER all use-cases have registered
71
- # (Zeitwerk.eager_load runs in lib/textus.rb, then session.rb is explicitly
72
- # required so UseCase.entries is fully populated).
73
- Application::UseCase.each do |entry|
74
- verb = entry.verb
75
- mod = entry.mod
76
- caps_sym = entry.caps_kind
77
-
78
- define_method(verb) do |*args, **kwargs|
79
- fixed = { session: self, ctx: @ctx, caps: caps_sym == :read ? @read_caps : @write_caps }
80
- mod.call(*args, **fixed, **kwargs)
81
- end
82
- end
83
- end
84
- end