textus 0.30.0 → 0.35.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +2 -241
  3. data/CHANGELOG.md +113 -0
  4. data/README.md +83 -62
  5. data/SPEC.md +352 -211
  6. data/docs/conventions.md +42 -37
  7. data/lib/textus/boot.rb +89 -74
  8. data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
  9. data/lib/textus/cli/verb/build.rb +1 -1
  10. data/lib/textus/cli/verb/fetch.rb +14 -0
  11. data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
  12. data/lib/textus/cli/verb/get.rb +1 -1
  13. data/lib/textus/cli/verb/hooks.rb +1 -1
  14. data/lib/textus/cli/verb/put.rb +1 -1
  15. data/lib/textus/cli/verb/rule_list.rb +7 -7
  16. data/lib/textus/cli.rb +2 -2
  17. data/lib/textus/container.rb +1 -2
  18. data/lib/textus/dispatcher.rb +3 -3
  19. data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
  20. data/lib/textus/doctor/check/proposal_targets.rb +45 -0
  21. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  22. data/lib/textus/doctor.rb +2 -1
  23. data/lib/textus/domain/action.rb +3 -3
  24. data/lib/textus/domain/freshness/evaluator.rb +3 -3
  25. data/lib/textus/domain/freshness/policy.rb +2 -2
  26. data/lib/textus/domain/freshness.rb +7 -7
  27. data/lib/textus/domain/outcome.rb +2 -2
  28. data/lib/textus/domain/permission.rb +2 -10
  29. data/lib/textus/domain/policy/base_guards.rb +25 -0
  30. data/lib/textus/domain/policy/evaluation.rb +18 -0
  31. data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
  32. data/lib/textus/domain/policy/guard.rb +35 -0
  33. data/lib/textus/domain/policy/guard_factory.rb +40 -0
  34. data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
  35. data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
  36. data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
  37. data/lib/textus/domain/policy/predicates/registry.rb +39 -0
  38. data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
  39. data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
  40. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
  41. data/lib/textus/domain/staleness/intake_check.rb +6 -6
  42. data/lib/textus/envelope.rb +2 -2
  43. data/lib/textus/errors.rb +25 -28
  44. data/lib/textus/hooks/event_bus.rb +4 -4
  45. data/lib/textus/init.rb +23 -18
  46. data/lib/textus/maintenance/zone_mv.rb +1 -1
  47. data/lib/textus/manifest/capabilities.rb +29 -0
  48. data/lib/textus/manifest/data.rb +14 -10
  49. data/lib/textus/manifest/policy.rb +37 -21
  50. data/lib/textus/manifest/rules.rb +16 -14
  51. data/lib/textus/manifest/schema.rb +48 -58
  52. data/lib/textus/manifest.rb +3 -3
  53. data/lib/textus/mcp/server.rb +1 -1
  54. data/lib/textus/mcp/tool_schemas.rb +3 -3
  55. data/lib/textus/mcp/tools.rb +7 -7
  56. data/lib/textus/ports/audit_subscriber.rb +1 -1
  57. data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
  58. data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
  59. data/lib/textus/projection.rb +1 -1
  60. data/lib/textus/read/freshness.rb +9 -9
  61. data/lib/textus/read/get.rb +8 -8
  62. data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
  63. data/lib/textus/read/policy_explain.rb +14 -10
  64. data/lib/textus/read/pulse.rb +5 -4
  65. data/lib/textus/read/validator.rb +1 -1
  66. data/lib/textus/schema/tools.rb +5 -5
  67. data/lib/textus/version.rb +1 -1
  68. data/lib/textus/write/accept.rb +19 -55
  69. data/lib/textus/write/delete.rb +14 -2
  70. data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
  71. data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
  72. data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +21 -14
  73. data/lib/textus/write/mv.rb +15 -3
  74. data/lib/textus/write/put.rb +14 -2
  75. data/lib/textus/write/reject.rb +11 -5
  76. metadata +24 -18
  77. data/lib/textus/cli/verb/refresh.rb +0 -14
  78. data/lib/textus/domain/authorizer.rb +0 -37
  79. data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
  80. data/lib/textus/domain/policy/promote.rb +0 -26
  81. data/lib/textus/domain/policy/promotion.rb +0 -57
  82. data/lib/textus/manifest/role_kinds.rb +0 -21
  83. data/lib/textus/write/authority_gate.rb +0 -24
@@ -1,8 +1,8 @@
1
1
  module Textus
2
2
  module Write
3
- class RefreshOrchestrator
4
- # Collaborator (not a Dispatcher verb): constructed directly by RefreshWorker /
5
- # GetOrRefresh, which pass their derived hook_context in. That's why this takes
3
+ class FetchOrchestrator
4
+ # Collaborator (not a Dispatcher verb): constructed directly by FetchWorker /
5
+ # GetOrFetch, which pass their derived hook_context in. That's why this takes
6
6
  # hook_context: explicitly while verb use cases derive their own.
7
7
  def initialize(worker:, store_root:, events:, hook_context: nil, detached_spawner: nil)
8
8
  @worker = worker
@@ -14,9 +14,9 @@ module Textus
14
14
 
15
15
  def execute(action, key:)
16
16
  case action
17
- when Textus::Domain::Action::Return then Textus::Domain::Outcome::Skipped.new
18
- when Textus::Domain::Action::RefreshSync then run_sync(key)
19
- when Textus::Domain::Action::RefreshTimed then run_timed(action.budget_ms, key)
17
+ when Textus::Domain::Action::Return then Textus::Domain::Outcome::Skipped.new
18
+ when Textus::Domain::Action::FetchSync then run_sync(key)
19
+ when Textus::Domain::Action::FetchTimed then run_timed(action.budget_ms, key)
20
20
  else raise ArgumentError.new("unknown action: #{action.inspect}")
21
21
  end
22
22
  end
@@ -25,13 +25,13 @@ module Textus
25
25
 
26
26
  def run_sync(key)
27
27
  envelope = @worker.run(key)
28
- Textus::Domain::Outcome::Refreshed.new(envelope: envelope)
28
+ Textus::Domain::Outcome::Fetched.new(envelope: envelope)
29
29
  rescue Textus::Error => e
30
30
  Textus::Domain::Outcome::Failed.new(error: e)
31
31
  end
32
32
 
33
33
  def run_timed(budget_ms, key)
34
- return run_timed_with_fork(budget_ms, key) if Textus::Ports::Refresh::Detached.supported?
34
+ return run_timed_with_fork(budget_ms, key) if Textus::Ports::Fetch::Detached.supported?
35
35
 
36
36
  run_timed_cooperative(budget_ms, key)
37
37
  end
@@ -49,7 +49,7 @@ module Textus
49
49
  thread.kill
50
50
  return Textus::Domain::Outcome::Failed.new(
51
51
  error: Textus::UsageError.new(
52
- "refresh exceeded budget #{budget_ms}ms (no fork available — cooperative cancel)",
52
+ "fetch exceeded budget #{budget_ms}ms (no fork available — cooperative cancel)",
53
53
  ),
54
54
  )
55
55
  end
@@ -57,7 +57,7 @@ module Textus
57
57
  if result.is_a?(Textus::Error)
58
58
  Textus::Domain::Outcome::Failed.new(error: result)
59
59
  else
60
- Textus::Domain::Outcome::Refreshed.new(envelope: result)
60
+ Textus::Domain::Outcome::Fetched.new(envelope: result)
61
61
  end
62
62
  end
63
63
 
@@ -77,25 +77,25 @@ module Textus
77
77
  # Single-flight: if a sibling process / earlier fork holds the
78
78
  # per-leaf lock, don't fork another worker — they're already
79
79
  # doing this work.
80
- probe = Textus::Ports::Refresh::Lock.new(root: @store_root, key: key)
80
+ probe = Textus::Ports::Fetch::Lock.new(root: @store_root, key: key)
81
81
  return Textus::Domain::Outcome::Detached.new unless probe.try_acquire
82
82
 
83
83
  probe.release
84
84
 
85
85
  payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
86
86
  payload[:ctx] = @hook_context if @hook_context
87
- @events.publish(:refresh_backgrounded, **payload)
87
+ @events.publish(:fetch_backgrounded, **payload)
88
88
  @detached_spawner.call(store_root: @store_root, key: key)
89
89
  Textus::Domain::Outcome::Detached.new
90
90
  elsif result.is_a?(Textus::Error)
91
91
  Textus::Domain::Outcome::Failed.new(error: result)
92
92
  else
93
- Textus::Domain::Outcome::Refreshed.new(envelope: result)
93
+ Textus::Domain::Outcome::Fetched.new(envelope: result)
94
94
  end
95
95
  end
96
96
 
97
97
  def default_spawner
98
- Textus::Ports::Refresh::Detached.method(:spawn)
98
+ Textus::Ports::Fetch::Detached.method(:spawn)
99
99
  end
100
100
  end
101
101
  end
@@ -2,20 +2,20 @@ require "timeout"
2
2
 
3
3
  module Textus
4
4
  module Write
5
- class RefreshWorker
5
+ class FetchWorker
6
6
  FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
7
7
 
8
8
  def initialize(container:, call:)
9
9
  @container = container
10
10
  @call = call
11
11
  @manifest = container.manifest
12
+ @schemas = container.schemas
12
13
  @events = container.events
13
14
  @rpc = container.rpc
14
- @authorizer = container.authorizer
15
15
  end
16
16
 
17
17
  # call(key) is the primary entry; run is kept as an alias for
18
- # Orchestrator and RefreshAll which call worker.run(key).
18
+ # Orchestrator and FetchAll which call worker.run(key).
19
19
  def call(key)
20
20
  run(key)
21
21
  end
@@ -63,11 +63,11 @@ module Textus
63
63
 
64
64
  def fetch_timeout_for(key)
65
65
  rule = @manifest.rules.for(key)
66
- rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
66
+ rule&.fetch&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
67
67
  end
68
68
 
69
69
  def fetch_with_events(key, mentry, remaining)
70
- @events.publish(:refresh_started, ctx: hook_context, key: key, mode: :sync)
70
+ @events.publish(:fetch_started, ctx: hook_context, key: key, mode: :sync)
71
71
  call_intake(key, mentry, remaining)
72
72
  end
73
73
 
@@ -80,23 +80,30 @@ module Textus
80
80
  args: { trigger_key: key, leaf_segments: remaining || [] })
81
81
  end
82
82
  rescue Timeout::Error
83
- @events.publish(:refresh_failed, ctx: hook_context, key: key,
84
- error_class: "Timeout::Error",
85
- error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
83
+ @events.publish(:fetch_failed, ctx: hook_context, key: key,
84
+ error_class: "Timeout::Error",
85
+ error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
86
86
  raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
87
87
  rescue Textus::Error => e
88
- @events.publish(:refresh_failed, ctx: hook_context, key: key, error_class: e.class.name,
89
- error_message: e.message)
88
+ @events.publish(:fetch_failed, ctx: hook_context, key: key, error_class: e.class.name,
89
+ error_message: e.message)
90
90
  raise
91
91
  rescue StandardError => e
92
- @events.publish(:refresh_failed, ctx: hook_context, key: key, error_class: e.class.name,
93
- error_message: e.message)
92
+ @events.publish(:fetch_failed, ctx: hook_context, key: key, error_class: e.class.name,
93
+ error_message: e.message)
94
94
  raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
95
95
  end
96
96
 
97
97
  def persist_and_notify(key, mentry, result, before_etag)
98
98
  normalized = self.class.normalize_action_result(result, format: mentry.format)
99
- @authorizer.authorize_write!(mentry, role: @call.role)
99
+ Textus::Domain::Policy::GuardFactory.new(
100
+ manifest: @manifest, schemas: @schemas,
101
+ ).for(:fetch, key).check!(
102
+ Textus::Domain::Policy::Evaluation.new(
103
+ actor: @call.role, transition: :fetch, origin: nil,
104
+ target: key, envelope: nil, snapshot: @manifest
105
+ ),
106
+ )
100
107
  envelope = writer.put(
101
108
  key,
102
109
  mentry: mentry,
@@ -105,7 +112,7 @@ module Textus
105
112
  ),
106
113
  )
107
114
  change = detect_change(before_etag, envelope)
108
- @events.publish(:entry_refreshed, ctx: hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
115
+ @events.publish(:entry_fetched, ctx: hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
109
116
  envelope
110
117
  end
111
118
 
@@ -6,7 +6,6 @@ module Textus
6
6
  @call = call
7
7
  @manifest = container.manifest
8
8
  @events = container.events
9
- @authorizer = container.authorizer
10
9
  end
11
10
 
12
11
  def call(old_key, new_key, dry_run: false)
@@ -38,8 +37,8 @@ module Textus
38
37
  raise UnknownKey.new(old_key) unless reader.exists?(old_key)
39
38
 
40
39
  validate_zone_and_format!(old_res.entry, new_res.entry)
41
- @authorizer.authorize_write!(old_res.entry, role: @call.role)
42
- @authorizer.authorize_write!(new_res.entry, role: @call.role)
40
+ guard_for(:mv, old_key).check!(eval_for(:mv, target_key: old_key))
41
+ guard_for(:mv, new_key).check!(eval_for(:mv, target_key: new_key))
43
42
  raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
44
43
 
45
44
  [old_res, new_res]
@@ -101,6 +100,19 @@ module Textus
101
100
  }
102
101
  end
103
102
 
103
+ def guard_for(transition, key, if_etag: nil)
104
+ Textus::Domain::Policy::GuardFactory.new(
105
+ manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
106
+ ).for(transition, key)
107
+ end
108
+
109
+ def eval_for(transition, target_key:, envelope: nil)
110
+ Textus::Domain::Policy::Evaluation.new(
111
+ actor: @call.role, transition: transition, origin: nil,
112
+ target: target_key, envelope: envelope, snapshot: @manifest
113
+ )
114
+ end
115
+
104
116
  def writer
105
117
  @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
106
118
  end
@@ -5,14 +5,13 @@ module Textus
5
5
  @container = container
6
6
  @call = call
7
7
  @manifest = container.manifest
8
- @authorizer = container.authorizer
9
8
  @events = container.events
10
9
  end
11
10
 
12
11
  def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
13
12
  Textus::Manifest::Data.validate_key!(key)
14
13
  mentry = @manifest.resolver.resolve(key).entry
15
- @authorizer.authorize_write!(mentry, role: @call.role)
14
+ guard_for(:put, key, if_etag: if_etag).check!(eval_for(:put, target_key: key))
16
15
 
17
16
  envelope = writer.put(
18
17
  key,
@@ -33,6 +32,19 @@ module Textus
33
32
 
34
33
  private
35
34
 
35
+ def guard_for(transition, key, if_etag: nil)
36
+ Textus::Domain::Policy::GuardFactory.new(
37
+ manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
38
+ ).for(transition, key)
39
+ end
40
+
41
+ def eval_for(transition, target_key:, envelope: nil)
42
+ Textus::Domain::Policy::Evaluation.new(
43
+ actor: @call.role, transition: transition, origin: nil,
44
+ target: target_key, envelope: envelope, snapshot: @manifest
45
+ )
46
+ end
47
+
36
48
  def hook_context
37
49
  @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
38
50
  end
@@ -1,19 +1,21 @@
1
- require_relative "authority_gate"
2
-
3
1
  module Textus
4
2
  module Write
5
3
  class Reject
6
- include AuthorityGate
7
-
8
4
  def initialize(container:, call:)
9
5
  @container = container
10
6
  @call = call
11
7
  @manifest = container.manifest
8
+ @schemas = container.schemas
12
9
  @events = container.events
13
10
  end
14
11
 
15
12
  def call(pending_key)
16
- assert_accept_authority!("reject")
13
+ guard.for(:reject, pending_key).check!(
14
+ Textus::Domain::Policy::Evaluation.new(
15
+ actor: @call.role, transition: :reject, origin: pending_key,
16
+ target: pending_key, envelope: nil, snapshot: @manifest
17
+ ),
18
+ )
17
19
 
18
20
  mentry = @manifest.resolver.resolve(pending_key).entry
19
21
  unless mentry.in_proposal_zone?(@manifest.policy)
@@ -40,6 +42,10 @@ module Textus
40
42
 
41
43
  private
42
44
 
45
+ def guard
46
+ @guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
47
+ end
48
+
43
49
  def hook_context
44
50
  @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
45
51
  end
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.30.0
4
+ version: 0.35.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -119,10 +119,10 @@ files:
119
119
  - lib/textus/call.rb
120
120
  - lib/textus/cli.rb
121
121
  - lib/textus/cli/group.rb
122
+ - lib/textus/cli/group/fetch.rb
122
123
  - lib/textus/cli/group/hook.rb
123
124
  - lib/textus/cli/group/key.rb
124
125
  - lib/textus/cli/group/mcp.rb
125
- - lib/textus/cli/group/refresh.rb
126
126
  - lib/textus/cli/group/rule.rb
127
127
  - lib/textus/cli/group/schema.rb
128
128
  - lib/textus/cli/group/zone.rb
@@ -135,6 +135,8 @@ files:
135
135
  - lib/textus/cli/verb/delete.rb
136
136
  - lib/textus/cli/verb/deps.rb
137
137
  - lib/textus/cli/verb/doctor.rb
138
+ - lib/textus/cli/verb/fetch.rb
139
+ - lib/textus/cli/verb/fetch_stale.rb
138
140
  - lib/textus/cli/verb/freshness.rb
139
141
  - lib/textus/cli/verb/get.rb
140
142
  - lib/textus/cli/verb/hook_run.rb
@@ -149,8 +151,6 @@ files:
149
151
  - lib/textus/cli/verb/pulse.rb
150
152
  - lib/textus/cli/verb/put.rb
151
153
  - lib/textus/cli/verb/rdeps.rb
152
- - lib/textus/cli/verb/refresh.rb
153
- - lib/textus/cli/verb/refresh_stale.rb
154
154
  - lib/textus/cli/verb/reject.rb
155
155
  - lib/textus/cli/verb/retain.rb
156
156
  - lib/textus/cli/verb/rule_explain.rb
@@ -168,13 +168,14 @@ files:
168
168
  - lib/textus/doctor.rb
169
169
  - lib/textus/doctor/check.rb
170
170
  - lib/textus/doctor/check/audit_log.rb
171
+ - lib/textus/doctor/check/fetch_locks.rb
171
172
  - lib/textus/doctor/check/handler_allowlist.rb
172
173
  - lib/textus/doctor/check/hooks.rb
173
174
  - lib/textus/doctor/check/illegal_keys.rb
174
175
  - lib/textus/doctor/check/intake_registration.rb
175
176
  - lib/textus/doctor/check/manifest_files.rb
177
+ - lib/textus/doctor/check/proposal_targets.rb
176
178
  - lib/textus/doctor/check/protocol_version.rb
177
- - lib/textus/doctor/check/refresh_locks.rb
178
179
  - lib/textus/doctor/check/rule_ambiguity.rb
179
180
  - lib/textus/doctor/check/schema_parse_error.rb
180
181
  - lib/textus/doctor/check/schema_violations.rb
@@ -183,7 +184,6 @@ files:
183
184
  - lib/textus/doctor/check/templates.rb
184
185
  - lib/textus/doctor/check/unowned_schema_fields.rb
185
186
  - lib/textus/domain/action.rb
186
- - lib/textus/domain/authorizer.rb
187
187
  - lib/textus/domain/duration.rb
188
188
  - lib/textus/domain/freshness.rb
189
189
  - lib/textus/domain/freshness/evaluator.rb
@@ -191,13 +191,20 @@ files:
191
191
  - lib/textus/domain/freshness/verdict.rb
192
192
  - lib/textus/domain/outcome.rb
193
193
  - lib/textus/domain/permission.rb
194
+ - lib/textus/domain/policy/base_guards.rb
195
+ - lib/textus/domain/policy/evaluation.rb
196
+ - lib/textus/domain/policy/fetch.rb
197
+ - lib/textus/domain/policy/guard.rb
198
+ - lib/textus/domain/policy/guard_factory.rb
194
199
  - lib/textus/domain/policy/handler_allowlist.rb
195
200
  - lib/textus/domain/policy/matcher.rb
196
- - lib/textus/domain/policy/predicates/accept_authority_signed.rb
201
+ - lib/textus/domain/policy/predicates/author_held.rb
202
+ - lib/textus/domain/policy/predicates/etag_match.rb
203
+ - lib/textus/domain/policy/predicates/fresh_within.rb
204
+ - lib/textus/domain/policy/predicates/registry.rb
197
205
  - lib/textus/domain/policy/predicates/schema_valid.rb
198
- - lib/textus/domain/policy/promote.rb
199
- - lib/textus/domain/policy/promotion.rb
200
- - lib/textus/domain/policy/refresh.rb
206
+ - lib/textus/domain/policy/predicates/target_is_canon.rb
207
+ - lib/textus/domain/policy/predicates/zone_writable_by.rb
201
208
  - lib/textus/domain/policy/retention.rb
202
209
  - lib/textus/domain/retention.rb
203
210
  - lib/textus/domain/sentinel.rb
@@ -234,6 +241,7 @@ files:
234
241
  - lib/textus/maintenance/rule_lint.rb
235
242
  - lib/textus/maintenance/zone_mv.rb
236
243
  - lib/textus/manifest.rb
244
+ - lib/textus/manifest/capabilities.rb
237
245
  - lib/textus/manifest/data.rb
238
246
  - lib/textus/manifest/entry.rb
239
247
  - lib/textus/manifest/entry/base.rb
@@ -250,7 +258,6 @@ files:
250
258
  - lib/textus/manifest/entry/validators/publish_each.rb
251
259
  - lib/textus/manifest/policy.rb
252
260
  - lib/textus/manifest/resolver.rb
253
- - lib/textus/manifest/role_kinds.rb
254
261
  - lib/textus/manifest/rules.rb
255
262
  - lib/textus/manifest/schema.rb
256
263
  - lib/textus/mcp.rb
@@ -264,9 +271,9 @@ files:
264
271
  - lib/textus/ports/audit_subscriber.rb
265
272
  - lib/textus/ports/build_lock.rb
266
273
  - lib/textus/ports/clock.rb
274
+ - lib/textus/ports/fetch/detached.rb
275
+ - lib/textus/ports/fetch/lock.rb
267
276
  - lib/textus/ports/publisher.rb
268
- - lib/textus/ports/refresh/detached.rb
269
- - lib/textus/ports/refresh/lock.rb
270
277
  - lib/textus/ports/sentinel_store.rb
271
278
  - lib/textus/ports/storage/file_stat.rb
272
279
  - lib/textus/ports/storage/file_store.rb
@@ -278,7 +285,7 @@ files:
278
285
  - lib/textus/read/doctor.rb
279
286
  - lib/textus/read/freshness.rb
280
287
  - lib/textus/read/get.rb
281
- - lib/textus/read/get_or_refresh.rb
288
+ - lib/textus/read/get_or_fetch.rb
282
289
  - lib/textus/read/list.rb
283
290
  - lib/textus/read/policy_explain.rb
284
291
  - lib/textus/read/published.rb
@@ -300,16 +307,15 @@ files:
300
307
  - lib/textus/uid.rb
301
308
  - lib/textus/version.rb
302
309
  - lib/textus/write/accept.rb
303
- - lib/textus/write/authority_gate.rb
304
310
  - lib/textus/write/delete.rb
311
+ - lib/textus/write/fetch_all.rb
312
+ - lib/textus/write/fetch_orchestrator.rb
313
+ - lib/textus/write/fetch_worker.rb
305
314
  - lib/textus/write/intake_fetch.rb
306
315
  - lib/textus/write/materializer.rb
307
316
  - lib/textus/write/mv.rb
308
317
  - lib/textus/write/publish.rb
309
318
  - lib/textus/write/put.rb
310
- - lib/textus/write/refresh_all.rb
311
- - lib/textus/write/refresh_orchestrator.rb
312
- - lib/textus/write/refresh_worker.rb
313
319
  - lib/textus/write/reject.rb
314
320
  - lib/textus/write/retention_sweep.rb
315
321
  homepage: https://github.com/patrick204nqh/textus
@@ -1,14 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Refresh < Verb
5
- option :as_flag, "--as=ROLE"
6
-
7
- def call(store)
8
- key = positional.shift or raise UsageError.new("refresh requires a key")
9
- emit(session_for(store).refresh(key).to_h_for_wire)
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Textus
4
- module Domain
5
- # Authorization service. Single source of truth for "given a manifest
6
- # entry and a role, may this caller read/write?". Lives in Domain
7
- # alongside Permission.
8
- class Authorizer
9
- def initialize(manifest:)
10
- @manifest = manifest
11
- end
12
-
13
- def can_write?(zone, role:)
14
- @manifest.policy.permission_for(zone.to_s).allows_write?(role)
15
- end
16
-
17
- def can_read?(zone, role:)
18
- @manifest.policy.permission_for(zone.to_s).allows_read?(role)
19
- end
20
-
21
- def authorize_write!(mentry, role:)
22
- return if can_write?(mentry.zone, role: role)
23
-
24
- writers = @manifest.policy.zone_writers(mentry.zone)
25
- raise WriteForbidden.new(mentry.key, mentry.zone, writers: writers)
26
- end
27
-
28
- def authorize_read!(mentry, role:)
29
- return if can_read?(mentry.zone, role: role)
30
-
31
- readers = @manifest.policy.zone_readers[mentry.zone]
32
- readers = nil if readers == :all
33
- raise ReadForbidden.new(mentry.key, mentry.zone, readers: readers)
34
- end
35
- end
36
- end
37
- end
@@ -1,33 +0,0 @@
1
- module Textus
2
- module Domain
3
- module Policy
4
- module Predicates
5
- # Promotion predicate: the role driving the promotion must have
6
- # role_kind == :accept_authority in the active manifest.
7
- #
8
- # Accept/Reject already gate on this kind before reaching the
9
- # promotion policy, so in the default control-flow this predicate
10
- # trivially passes. It is kept so manifests can express the
11
- # requirement explicitly in `rules[].promotion.requires`.
12
- class AcceptAuthoritySigned
13
- attr_reader :reason
14
-
15
- def name
16
- "accept_authority_signed"
17
- end
18
-
19
- def call(role:, manifest:, entry: nil) # rubocop:disable Lint/UnusedMethodArgument
20
- role_str = role&.to_s
21
- return true if role_str.nil? || role_str.empty?
22
-
23
- kind = manifest.policy.role_kind(role_str)
24
- return true if kind == :accept_authority
25
-
26
- @reason = "role '#{role_str}' has kind '#{kind.inspect}', expected ':accept_authority'"
27
- false
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Domain
3
- module Policy
4
- class Promote
5
- KNOWN = %i[schema_valid accept_authority_signed].freeze
6
- attr_reader :requires
7
-
8
- def initialize(requires:)
9
- syms = Array(requires).map { |r| r.to_s.to_sym }
10
- unknown = syms - KNOWN
11
- unless unknown.empty?
12
- raise Textus::UsageError.new(
13
- "unknown promote requirement: #{unknown.first.inspect} (known: #{KNOWN.join(", ")})",
14
- )
15
- end
16
-
17
- @requires = syms
18
- end
19
-
20
- def demands?(req)
21
- @requires.include?(req)
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,57 +0,0 @@
1
- require_relative "predicates/schema_valid"
2
- require_relative "predicates/accept_authority_signed"
3
-
4
- module Textus
5
- module Domain
6
- module Policy
7
- class Promotion
8
- Result = Struct.new(:ok?, :reasons, keyword_init: true)
9
-
10
- REGISTRY = {
11
- "schema_valid" => -> { Predicates::SchemaValid.new },
12
- "accept_authority_signed" => -> { Predicates::AcceptAuthoritySigned.new },
13
- }.freeze
14
-
15
- def self.from_names(names)
16
- predicates = Array(names).map do |n|
17
- ctor = REGISTRY[n.to_s] or raise Textus::UsageError.new(
18
- "unknown promotion predicate: '#{n}' (known: #{REGISTRY.keys.join(", ")})",
19
- )
20
- ctor.call
21
- end
22
- new(predicates: predicates)
23
- end
24
-
25
- attr_reader :predicates
26
-
27
- def initialize(predicates:)
28
- @predicates = predicates
29
- end
30
-
31
- def predicate_names
32
- @predicates.map(&:name)
33
- end
34
-
35
- def evaluate(entry:, schemas:, manifest:, role:)
36
- reasons = []
37
- @predicates.each do |pred|
38
- ok = invoke(pred, entry: entry, schemas: schemas, manifest: manifest, role: role)
39
- reasons << "#{pred.name}: #{pred.reason || "predicate failed"}" unless ok
40
- end
41
- Result.new(ok?: reasons.empty?, reasons: reasons)
42
- end
43
-
44
- private
45
-
46
- def invoke(pred, entry:, schemas:, manifest:, role:)
47
- case pred.name
48
- when "accept_authority_signed"
49
- pred.call(role: role, manifest: manifest, entry: entry)
50
- else
51
- pred.call(entry: entry, schemas: schemas, manifest: manifest)
52
- end
53
- end
54
- end
55
- end
56
- end
57
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- class Manifest
3
- module RoleKinds
4
- DEFAULT_MAPPING = {
5
- "human" => :accept_authority,
6
- "agent" => :proposer,
7
- "builder" => :generator,
8
- "runner" => :runner,
9
- }.freeze
10
-
11
- # Returns { role_name => kind_symbol }. When `roles:` is declared we use
12
- # exactly that; defaults are *not* layered in (declaring roles is an opt-in
13
- # to a fully user-defined vocabulary).
14
- def self.resolve(raw_roles)
15
- return DEFAULT_MAPPING if raw_roles.nil?
16
-
17
- raw_roles.to_h { |r| [r["name"], r["kind"].to_sym] }.freeze
18
- end
19
- end
20
- end
21
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- module Write
3
- # Shared gate for write verbs that require the caller to hold the
4
- # manifest's accept_authority role. Provides one method, expressed
5
- # as two early-returns rather than a ternary, so each failure mode
6
- # reads on its own line.
7
- module AuthorityGate
8
- def assert_accept_authority!(verb)
9
- return if @manifest.policy.role_kind(@call.role) == :accept_authority
10
-
11
- authority = @manifest.policy.roles_with_kind(:accept_authority).first
12
- if authority.nil?
13
- raise ProposalError.new(
14
- "no role with accept_authority kind is declared in this manifest; #{verb} is disabled",
15
- )
16
- end
17
-
18
- raise ProposalError.new(
19
- "only #{authority} role can #{verb} proposals; got '#{@call.role}'",
20
- )
21
- end
22
- end
23
- end
24
- end