textus 0.29.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +2 -235
  3. data/CHANGELOG.md +169 -0
  4. data/README.md +85 -64
  5. data/SPEC.md +366 -201
  6. data/docs/conventions.md +42 -37
  7. data/lib/textus/boot.rb +93 -76
  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/hook_run.rb +2 -6
  14. data/lib/textus/cli/verb/hooks.rb +1 -1
  15. data/lib/textus/cli/verb/put.rb +5 -14
  16. data/lib/textus/cli/verb/retain.rb +19 -0
  17. data/lib/textus/cli/verb/rule_list.rb +8 -8
  18. data/lib/textus/cli.rb +21 -18
  19. data/lib/textus/container.rb +1 -2
  20. data/lib/textus/dispatcher.rb +11 -3
  21. data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
  22. data/lib/textus/doctor/check/proposal_targets.rb +45 -0
  23. data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
  24. data/lib/textus/doctor/check.rb +8 -5
  25. data/lib/textus/doctor.rb +2 -1
  26. data/lib/textus/domain/action.rb +3 -3
  27. data/lib/textus/domain/duration.rb +22 -0
  28. data/lib/textus/domain/freshness/evaluator.rb +3 -3
  29. data/lib/textus/domain/freshness/policy.rb +2 -2
  30. data/lib/textus/domain/freshness.rb +7 -7
  31. data/lib/textus/domain/outcome.rb +2 -2
  32. data/lib/textus/domain/permission.rb +2 -10
  33. data/lib/textus/domain/policy/base_guards.rb +25 -0
  34. data/lib/textus/domain/policy/evaluation.rb +18 -0
  35. data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +2 -16
  36. data/lib/textus/domain/policy/guard.rb +35 -0
  37. data/lib/textus/domain/policy/guard_factory.rb +40 -0
  38. data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
  39. data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
  40. data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
  41. data/lib/textus/domain/policy/predicates/registry.rb +39 -0
  42. data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
  43. data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
  44. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
  45. data/lib/textus/domain/policy/retention.rb +26 -0
  46. data/lib/textus/domain/retention.rb +44 -0
  47. data/lib/textus/domain/staleness/intake_check.rb +6 -6
  48. data/lib/textus/envelope/io/reader.rb +4 -0
  49. data/lib/textus/envelope/io/writer.rb +8 -0
  50. data/lib/textus/envelope.rb +2 -2
  51. data/lib/textus/errors.rb +25 -28
  52. data/lib/textus/hooks/event_bus.rb +12 -24
  53. data/lib/textus/hooks/rpc_registry.rb +9 -35
  54. data/lib/textus/hooks/signature.rb +31 -0
  55. data/lib/textus/init.rb +24 -18
  56. data/lib/textus/maintenance/zone_mv.rb +1 -1
  57. data/lib/textus/manifest/capabilities.rb +29 -0
  58. data/lib/textus/manifest/data.rb +16 -8
  59. data/lib/textus/manifest/entry/base.rb +2 -2
  60. data/lib/textus/manifest/policy.rb +62 -19
  61. data/lib/textus/manifest/rules.rb +25 -14
  62. data/lib/textus/manifest/schema.rb +78 -38
  63. data/lib/textus/manifest.rb +6 -5
  64. data/lib/textus/mcp/server.rb +2 -10
  65. data/lib/textus/mcp/session.rb +7 -23
  66. data/lib/textus/mcp/tool_schemas.rb +3 -3
  67. data/lib/textus/mcp/tools.rb +7 -7
  68. data/lib/textus/ports/audit_subscriber.rb +1 -1
  69. data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
  70. data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
  71. data/lib/textus/projection.rb +1 -1
  72. data/lib/textus/read/freshness.rb +9 -9
  73. data/lib/textus/read/get.rb +8 -8
  74. data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
  75. data/lib/textus/read/policy_explain.rb +19 -10
  76. data/lib/textus/read/pulse.rb +5 -4
  77. data/lib/textus/read/retainable.rb +17 -0
  78. data/lib/textus/read/validator.rb +1 -1
  79. data/lib/textus/role_scope.rb +3 -2
  80. data/lib/textus/schema/tools.rb +5 -5
  81. data/lib/textus/version.rb +1 -1
  82. data/lib/textus/write/accept.rb +19 -55
  83. data/lib/textus/write/delete.rb +15 -17
  84. data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
  85. data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
  86. data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +23 -30
  87. data/lib/textus/write/intake_fetch.rb +23 -0
  88. data/lib/textus/write/mv.rb +17 -15
  89. data/lib/textus/write/put.rb +15 -17
  90. data/lib/textus/write/reject.rb +11 -5
  91. data/lib/textus/write/retention_sweep.rb +55 -0
  92. metadata +32 -18
  93. data/lib/textus/cli/verb/refresh.rb +0 -14
  94. data/lib/textus/domain/authorizer.rb +0 -37
  95. data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
  96. data/lib/textus/domain/policy/promote.rb +0 -26
  97. data/lib/textus/domain/policy/promotion.rb +0 -57
  98. data/lib/textus/manifest/role_kinds.rb +0 -21
  99. data/lib/textus/write/authority_gate.rb +0 -24
@@ -2,20 +2,20 @@ require "timeout"
2
2
 
3
3
  module Textus
4
4
  module Write
5
- class RefreshWorker
6
- FETCH_TIMEOUT_SECONDS = 30
5
+ class FetchWorker
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
 
@@ -117,21 +124,7 @@ module Textus
117
124
  end
118
125
 
119
126
  def writer
120
- @writer ||= Textus::Envelope::IO::Writer.new(
121
- file_store: @container.file_store,
122
- manifest: @container.manifest,
123
- schemas: @container.schemas,
124
- audit_log: @container.audit_log,
125
- call: @call,
126
- reader: reader,
127
- )
128
- end
129
-
130
- def reader
131
- @reader ||= Textus::Envelope::IO::Reader.new(
132
- file_store: @container.file_store,
133
- manifest: @container.manifest,
134
- )
127
+ @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
135
128
  end
136
129
  end
137
130
  end
@@ -0,0 +1,23 @@
1
+ require "timeout"
2
+
3
+ module Textus
4
+ module Write
5
+ # Invokes a :resolve_intake hook handler by name under a timeout.
6
+ # The transport-side fetch kernel shared by `textus put --fetch` and
7
+ # `textus hook run`. Maps Timeout::Error to a UsageError; leaves any
8
+ # other error to the caller (call sites differ in how they wrap those).
9
+ module IntakeFetch
10
+ FETCH_TIMEOUT_SECONDS = 30
11
+
12
+ module_function
13
+
14
+ def invoke(rpc:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
15
+ Timeout.timeout(timeout) do
16
+ rpc.invoke(:resolve_intake, handler, caps: nil, config: config, args: args)
17
+ end
18
+ rescue Timeout::Error
19
+ raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -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,22 +100,25 @@ module Textus
101
100
  }
102
101
  end
103
102
 
104
- def writer
105
- @writer ||= Textus::Envelope::IO::Writer.new(
106
- file_store: @container.file_store,
107
- manifest: @container.manifest,
108
- schemas: @container.schemas,
109
- audit_log: @container.audit_log,
110
- call: @call,
111
- reader: reader,
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
112
113
  )
113
114
  end
114
115
 
116
+ def writer
117
+ @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
118
+ end
119
+
115
120
  def reader
116
- @reader ||= Textus::Envelope::IO::Reader.new(
117
- file_store: @container.file_store,
118
- manifest: @container.manifest,
119
- )
121
+ @reader ||= Textus::Envelope::IO::Reader.from(container: @container)
120
122
  end
121
123
  end
122
124
  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,26 +32,25 @@ module Textus
33
32
 
34
33
  private
35
34
 
36
- def hook_context
37
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
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)
38
39
  end
39
40
 
40
- def writer
41
- @writer ||= Textus::Envelope::IO::Writer.new(
42
- file_store: @container.file_store,
43
- manifest: @container.manifest,
44
- schemas: @container.schemas,
45
- audit_log: @container.audit_log,
46
- call: @call,
47
- reader: reader,
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
48
45
  )
49
46
  end
50
47
 
51
- def reader
52
- @reader ||= Textus::Envelope::IO::Reader.new(
53
- file_store: @container.file_store,
54
- manifest: @container.manifest,
55
- )
48
+ def hook_context
49
+ @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
50
+ end
51
+
52
+ def writer
53
+ @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
56
54
  end
57
55
  end
58
56
  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
@@ -0,0 +1,55 @@
1
+ require "fileutils"
2
+
3
+ module Textus
4
+ module Write
5
+ # Applies retention actions reported by Read::Retainable. `expire` deletes
6
+ # the leaf through the role gate; `archive` copies it to
7
+ # <root>/archive/<relative-path> first, then deletes. Rows whose zone the
8
+ # caller's role cannot write surface in `failed` rather than aborting.
9
+ class RetentionSweep
10
+ def initialize(container:, call:)
11
+ @container = container
12
+ @call = call
13
+ end
14
+
15
+ def call(prefix: nil, zone: nil)
16
+ rows = Textus::Read::Retainable.new(container: @container, call: @call)
17
+ .call(prefix: prefix, zone: zone)
18
+ delete_op = Textus::Write::Delete.new(container: @container, call: @call)
19
+ expired = []
20
+ archived = []
21
+ failed = []
22
+
23
+ rows.each do |row|
24
+ key = row["key"]
25
+ begin
26
+ archive_leaf(row) if row["action"] == "archive"
27
+ delete_op.call(key)
28
+ (row["action"] == "archive" ? archived : expired) << key
29
+ rescue Textus::Error => e
30
+ failed << { "key" => key, "error" => e.message }
31
+ end
32
+ end
33
+
34
+ {
35
+ "protocol" => Textus::PROTOCOL,
36
+ "ok" => failed.empty?,
37
+ "expired" => expired,
38
+ "archived" => archived,
39
+ "failed" => failed,
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def archive_leaf(row)
46
+ src = row["path"]
47
+ root = @container.root.to_s
48
+ rel = src.delete_prefix("#{root}/")
49
+ dest = File.join(root, "archive", rel)
50
+ FileUtils.mkdir_p(File.dirname(dest))
51
+ FileUtils.cp(src, dest)
52
+ end
53
+ end
54
+ end
55
+ 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.29.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,9 +151,8 @@ 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
+ - lib/textus/cli/verb/retain.rb
155
156
  - lib/textus/cli/verb/rule_explain.rb
156
157
  - lib/textus/cli/verb/rule_lint.rb
157
158
  - lib/textus/cli/verb/rule_list.rb
@@ -167,13 +168,14 @@ files:
167
168
  - lib/textus/doctor.rb
168
169
  - lib/textus/doctor/check.rb
169
170
  - lib/textus/doctor/check/audit_log.rb
171
+ - lib/textus/doctor/check/fetch_locks.rb
170
172
  - lib/textus/doctor/check/handler_allowlist.rb
171
173
  - lib/textus/doctor/check/hooks.rb
172
174
  - lib/textus/doctor/check/illegal_keys.rb
173
175
  - lib/textus/doctor/check/intake_registration.rb
174
176
  - lib/textus/doctor/check/manifest_files.rb
177
+ - lib/textus/doctor/check/proposal_targets.rb
175
178
  - lib/textus/doctor/check/protocol_version.rb
176
- - lib/textus/doctor/check/refresh_locks.rb
177
179
  - lib/textus/doctor/check/rule_ambiguity.rb
178
180
  - lib/textus/doctor/check/schema_parse_error.rb
179
181
  - lib/textus/doctor/check/schema_violations.rb
@@ -182,20 +184,29 @@ files:
182
184
  - lib/textus/doctor/check/templates.rb
183
185
  - lib/textus/doctor/check/unowned_schema_fields.rb
184
186
  - lib/textus/domain/action.rb
185
- - lib/textus/domain/authorizer.rb
187
+ - lib/textus/domain/duration.rb
186
188
  - lib/textus/domain/freshness.rb
187
189
  - lib/textus/domain/freshness/evaluator.rb
188
190
  - lib/textus/domain/freshness/policy.rb
189
191
  - lib/textus/domain/freshness/verdict.rb
190
192
  - lib/textus/domain/outcome.rb
191
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
192
199
  - lib/textus/domain/policy/handler_allowlist.rb
193
200
  - lib/textus/domain/policy/matcher.rb
194
- - 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
195
205
  - lib/textus/domain/policy/predicates/schema_valid.rb
196
- - lib/textus/domain/policy/promote.rb
197
- - lib/textus/domain/policy/promotion.rb
198
- - 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
208
+ - lib/textus/domain/policy/retention.rb
209
+ - lib/textus/domain/retention.rb
199
210
  - lib/textus/domain/sentinel.rb
200
211
  - lib/textus/domain/staleness.rb
201
212
  - lib/textus/domain/staleness/generator_check.rb
@@ -218,6 +229,7 @@ files:
218
229
  - lib/textus/hooks/fire_report.rb
219
230
  - lib/textus/hooks/loader.rb
220
231
  - lib/textus/hooks/rpc_registry.rb
232
+ - lib/textus/hooks/signature.rb
221
233
  - lib/textus/init.rb
222
234
  - lib/textus/key/distance.rb
223
235
  - lib/textus/key/grammar.rb
@@ -229,6 +241,7 @@ files:
229
241
  - lib/textus/maintenance/rule_lint.rb
230
242
  - lib/textus/maintenance/zone_mv.rb
231
243
  - lib/textus/manifest.rb
244
+ - lib/textus/manifest/capabilities.rb
232
245
  - lib/textus/manifest/data.rb
233
246
  - lib/textus/manifest/entry.rb
234
247
  - lib/textus/manifest/entry/base.rb
@@ -245,7 +258,6 @@ files:
245
258
  - lib/textus/manifest/entry/validators/publish_each.rb
246
259
  - lib/textus/manifest/policy.rb
247
260
  - lib/textus/manifest/resolver.rb
248
- - lib/textus/manifest/role_kinds.rb
249
261
  - lib/textus/manifest/rules.rb
250
262
  - lib/textus/manifest/schema.rb
251
263
  - lib/textus/mcp.rb
@@ -259,9 +271,9 @@ files:
259
271
  - lib/textus/ports/audit_subscriber.rb
260
272
  - lib/textus/ports/build_lock.rb
261
273
  - lib/textus/ports/clock.rb
274
+ - lib/textus/ports/fetch/detached.rb
275
+ - lib/textus/ports/fetch/lock.rb
262
276
  - lib/textus/ports/publisher.rb
263
- - lib/textus/ports/refresh/detached.rb
264
- - lib/textus/ports/refresh/lock.rb
265
277
  - lib/textus/ports/sentinel_store.rb
266
278
  - lib/textus/ports/storage/file_stat.rb
267
279
  - lib/textus/ports/storage/file_store.rb
@@ -273,12 +285,13 @@ files:
273
285
  - lib/textus/read/doctor.rb
274
286
  - lib/textus/read/freshness.rb
275
287
  - lib/textus/read/get.rb
276
- - lib/textus/read/get_or_refresh.rb
288
+ - lib/textus/read/get_or_fetch.rb
277
289
  - lib/textus/read/list.rb
278
290
  - lib/textus/read/policy_explain.rb
279
291
  - lib/textus/read/published.rb
280
292
  - lib/textus/read/pulse.rb
281
293
  - lib/textus/read/rdeps.rb
294
+ - lib/textus/read/retainable.rb
282
295
  - lib/textus/read/schema_envelope.rb
283
296
  - lib/textus/read/stale.rb
284
297
  - lib/textus/read/uid.rb
@@ -294,16 +307,17 @@ files:
294
307
  - lib/textus/uid.rb
295
308
  - lib/textus/version.rb
296
309
  - lib/textus/write/accept.rb
297
- - lib/textus/write/authority_gate.rb
298
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
314
+ - lib/textus/write/intake_fetch.rb
299
315
  - lib/textus/write/materializer.rb
300
316
  - lib/textus/write/mv.rb
301
317
  - lib/textus/write/publish.rb
302
318
  - lib/textus/write/put.rb
303
- - lib/textus/write/refresh_all.rb
304
- - lib/textus/write/refresh_orchestrator.rb
305
- - lib/textus/write/refresh_worker.rb
306
319
  - lib/textus/write/reject.rb
320
+ - lib/textus/write/retention_sweep.rb
307
321
  homepage: https://github.com/patrick204nqh/textus
308
322
  licenses:
309
323
  - MIT
@@ -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