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
@@ -8,7 +8,7 @@ module Textus
8
8
 
9
9
  # `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
10
10
  # semantics: pure read (`ops.get`) for materialization paths;
11
- # `ops.get_or_refresh` if you want refresh-on-stale.
11
+ # `ops.get_or_fetch` if you want fetch-on-stale.
12
12
  # `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
13
13
  # `rpc` — a `Hooks::RpcRegistry` used to dispatch `transform_rows` callables.
14
14
  # `transform_context` — capability object handed to transform reducers as `caps:`.
@@ -3,8 +3,8 @@ require "time"
3
3
  module Textus
4
4
  module Read
5
5
  # Per-entry freshness report. Walks every entry declared in the manifest,
6
- # consults `rules_for(key)` for a refresh rule, and reports the
7
- # current status. Status is one of :fresh, :stale, :never_refreshed, or
6
+ # consults `rules_for(key)` for a fetch rule, and reports the
7
+ # current status. Status is one of :fresh, :stale, :never_fetched, or
8
8
  # :no_policy.
9
9
  class Freshness
10
10
  def initialize(container:, call:, evaluator: Textus::Domain::Freshness::Evaluator)
@@ -16,7 +16,7 @@ module Textus
16
16
  @cache = {}
17
17
  end
18
18
 
19
- # Returns the soonest `next_due_at` across all entries with a refresh
19
+ # Returns the soonest `next_due_at` across all entries with a fetch
20
20
  # policy, as an ISO-8601 string, or nil if none.
21
21
  def soonest_due(prefix: nil, zone: nil)
22
22
  times = call(prefix: prefix, zone: zone)
@@ -43,17 +43,17 @@ module Textus
43
43
 
44
44
  def row_for(mentry)
45
45
  set = @manifest.rules.for(mentry.key)
46
- refresh = set.refresh
46
+ fetch = set.fetch
47
47
  envelope = safe_get(mentry.key)
48
- last = envelope&.meta&.dig("last_refreshed_at")
48
+ last = envelope&.meta&.dig("last_fetched_at")
49
49
 
50
- return base_row(mentry, last).merge(status: :no_policy) if refresh.nil?
50
+ return base_row(mentry, last).merge(status: :no_policy) if fetch.nil?
51
51
 
52
- fp = refresh.to_freshness_policy
52
+ fp = fetch.to_freshness_policy
53
53
  cache_key = [mentry.key, last]
54
54
  verdict = (@cache[cache_key] ||= @evaluator.call(fp, envelope, now: @call.now))
55
55
  status = if verdict.fresh? then :fresh
56
- elsif last.nil? then :never_refreshed
56
+ elsif last.nil? then :never_fetched
57
57
  else :stale
58
58
  end
59
59
 
@@ -69,7 +69,7 @@ module Textus
69
69
  {
70
70
  key: mentry.key,
71
71
  zone: mentry.zone,
72
- last_refreshed_at: last,
72
+ last_fetched_at: last,
73
73
  age_seconds: last ? (@call.now - Time.parse(last)).to_i : nil,
74
74
  }
75
75
  end
@@ -1,10 +1,10 @@
1
1
  module Textus
2
2
  module Read
3
3
  # Pure read: returns the on-disk envelope annotated with a freshness
4
- # verdict. Never triggers refresh; never invokes the orchestrator.
4
+ # verdict. Never triggers fetch; never invokes the orchestrator.
5
5
  #
6
- # For interactive reads that want refresh-on-stale, use
7
- # `Read::GetOrRefresh`, which composes this with the orchestrator.
6
+ # For interactive reads that want fetch-on-stale, use
7
+ # `Read::GetOrFetch`, which composes this with the orchestrator.
8
8
  class Get
9
9
  def initialize(container:, call:, evaluator: Textus::Domain::Freshness::Evaluator)
10
10
  @container = container
@@ -19,16 +19,16 @@ module Textus
19
19
  return nil if envelope.nil?
20
20
 
21
21
  policy_set = @manifest.rules.for(key)
22
- refresh_policy = policy_set.refresh
23
- return annotate_fresh(envelope) if refresh_policy.nil?
22
+ fetch_policy = policy_set.fetch
23
+ return annotate_fresh(envelope) if fetch_policy.nil?
24
24
 
25
- policy = refresh_policy.to_freshness_policy
25
+ policy = fetch_policy.to_freshness_policy
26
26
  verdict = @evaluator.call(policy, envelope, now: @call.now)
27
27
 
28
28
  envelope.with(freshness: Textus::Domain::Freshness.build(
29
29
  stale: verdict.stale?,
30
30
  reason: verdict.reason,
31
- refreshing: false,
31
+ fetching: false,
32
32
  ))
33
33
  end
34
34
 
@@ -58,7 +58,7 @@ module Textus
58
58
 
59
59
  def annotate_fresh(envelope)
60
60
  envelope.with(freshness: Textus::Domain::Freshness.build(
61
- stale: false, reason: nil, refreshing: false,
61
+ stale: false, reason: nil, fetching: false,
62
62
  ))
63
63
  end
64
64
  end
@@ -1,6 +1,6 @@
1
1
  module Textus
2
2
  module Read
3
- # Composes pure `Read::Get` with the refresh orchestrator: runs Get
3
+ # Composes pure `Read::Get` with the fetch orchestrator: runs Get
4
4
  # to obtain the envelope and freshness verdict, then if the verdict
5
5
  # is stale and the rule's `on_stale` policy demands action, hands
6
6
  # off to the orchestrator. Use for interactive reads where the
@@ -8,7 +8,7 @@ module Textus
8
8
  #
9
9
  # Pure reads (build, projection, schema tooling) should use
10
10
  # `Read::Get` directly; it has no orchestrator dependency.
11
- class GetOrRefresh
11
+ class GetOrFetch
12
12
  def initialize(container:, call:, get: nil, orchestrator: nil)
13
13
  @container = container
14
14
  @call = call
@@ -24,10 +24,10 @@ module Textus
24
24
  end
25
25
 
26
26
  def build_orchestrator
27
- worker = Textus::Write::RefreshWorker.new(
27
+ worker = Textus::Write::FetchWorker.new(
28
28
  container: @container, call: @call,
29
29
  )
30
- Textus::Write::RefreshOrchestrator.new(
30
+ Textus::Write::FetchOrchestrator.new(
31
31
  worker: worker, store_root: @container.root, events: @container.events,
32
32
  hook_context: hook_context
33
33
  )
@@ -41,10 +41,10 @@ module Textus
41
41
  return envelope unless envelope.freshness&.stale
42
42
 
43
43
  policy_set = @manifest.rules.for(key)
44
- refresh_policy = policy_set.refresh
45
- return envelope if refresh_policy.nil?
44
+ fetch_policy = policy_set.fetch
45
+ return envelope if fetch_policy.nil?
46
46
 
47
- policy = refresh_policy.to_freshness_policy
47
+ policy = fetch_policy.to_freshness_policy
48
48
  verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
49
49
  action = policy.decide(verdict)
50
50
  outcome = @orchestrator.execute(action, key: key)
@@ -52,15 +52,15 @@ module Textus
52
52
  case outcome
53
53
  when Textus::Domain::Outcome::Skipped
54
54
  envelope
55
- when Textus::Domain::Outcome::Refreshed
55
+ when Textus::Domain::Outcome::Fetched
56
56
  outcome.envelope.with(
57
- freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, refreshing: false),
57
+ freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, fetching: false),
58
58
  )
59
59
  when Textus::Domain::Outcome::Detached
60
- envelope.with(freshness: envelope.freshness.with(refreshing: true))
60
+ envelope.with(freshness: envelope.freshness.with(fetching: true))
61
61
  when Textus::Domain::Outcome::Failed
62
62
  envelope.with(
63
- freshness: envelope.freshness.with(refresh_error: outcome.error.message),
63
+ freshness: envelope.freshness.with(fetch_error: outcome.error.message),
64
64
  )
65
65
  end
66
66
  end
@@ -1,35 +1,44 @@
1
1
  module Textus
2
2
  module Read
3
3
  # For one key, surface every matching policy block along with the
4
- # per-slot effective value (which loses ties win-by-specificity).
4
+ # per-slot effective value (which loses ties win-by-specificity) and the
5
+ # effective guard predicate names for every write transition (ADR 0031).
5
6
  class PolicyExplain
6
7
  def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
7
8
  @manifest = container.manifest
9
+ @schemas = container.schemas
8
10
  end
9
11
 
10
12
  def call(key:)
11
- policies = @manifest.rules
12
- matching = policies.explain(key)
13
- winners = policies.for(key)
13
+ matching = @manifest.rules.explain(key)
14
+ winners = @manifest.rules.for(key)
15
+ factory = Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
14
16
 
15
17
  {
16
18
  key: key,
17
19
  matched_blocks: matching.map do |b|
18
20
  {
19
21
  match: b.match,
20
- refresh: !b.refresh.nil?,
22
+ fetch: !b.fetch.nil?,
21
23
  handler_allowlist: !b.handler_allowlist.nil?,
22
- promote: !b.promote.nil?,
24
+ guard: !b.guard.nil?,
25
+ retention: !b.retention.nil?,
23
26
  }
24
27
  end,
25
28
  effective: {
26
- refresh: winners.refresh && {
27
- ttl_seconds: winners.refresh.ttl_seconds,
28
- on_stale: winners.refresh.on_stale,
29
+ fetch: winners.fetch && {
30
+ ttl_seconds: winners.fetch.ttl_seconds,
31
+ on_stale: winners.fetch.on_stale,
29
32
  },
30
33
  handler_allowlist: winners.handler_allowlist&.handlers,
31
- promotion: winners.promote && { requires: winners.promote.requires },
34
+ retention: winners.retention && {
35
+ expire_after: winners.retention.expire_after,
36
+ archive_after: winners.retention.archive_after,
37
+ },
32
38
  },
39
+ guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
40
+ [transition, factory.for(transition, key).predicates.map(&:name)]
41
+ end,
33
42
  }
34
43
  end
35
44
  end
@@ -49,11 +49,12 @@ module Textus
49
49
  end
50
50
 
51
51
  def review_keys
52
- # List constructor takes only manifest:; returns hashes with string keys.
53
- # Guard: zones is a Hash keyed by name string.
54
- return [] unless @manifest.data.zones.key?("review")
52
+ # The single queue zone (kind: queue; schema guarantees ≤1), derived
53
+ # from the manifest rather than a hardcoded zone name (ADR 0034 / D1).
54
+ queue = @manifest.policy.queue_zone
55
+ return [] unless queue
55
56
 
56
- rows = Read::List.new(container: @container).call(zone: "review")
57
+ rows = Read::List.new(container: @container).call(zone: queue)
57
58
  rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
58
59
  end
59
60
 
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Read
3
+ class Retainable
4
+ def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
5
+ @manifest = container.manifest
6
+ end
7
+
8
+ def call(prefix: nil, zone: nil)
9
+ Textus::Domain::Retention.new(
10
+ manifest: @manifest,
11
+ file_stat: Textus::Ports::Storage::FileStat.new,
12
+ clock: Textus::Ports::Clock,
13
+ ).call(prefix: prefix, zone: zone)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -54,7 +54,7 @@ module Textus
54
54
  last_writer = @audit_log.last_writer_for(key)
55
55
  return if last_writer.nil?
56
56
 
57
- last_writer_is_authority = @manifest.policy.role_kind(last_writer) == :accept_authority
57
+ last_writer_is_authority = @manifest.policy.roles_with_capability("author").include?(last_writer)
58
58
 
59
59
  env.meta.each_key do |field|
60
60
  owner = schema.maintained_by(field)
@@ -38,11 +38,12 @@ module Textus
38
38
 
39
39
  Textus::Dispatcher::VERBS.each_key do |verb|
40
40
  define_method(verb) do |*args, **kwargs|
41
- klass = Textus::Dispatcher.fetch(verb)
42
41
  call_value = Textus::Call.build(
43
42
  role: @role, correlation_id: @correlation_id, dry_run: @dry_run,
44
43
  )
45
- klass.new(container: @container, call: call_value).call(*args, **kwargs)
44
+ Textus::Dispatcher.invoke(
45
+ verb, container: @container, call: call_value, args: args, kwargs: kwargs
46
+ )
46
47
  end
47
48
  end
48
49
  end
@@ -49,7 +49,7 @@ module Textus
49
49
  end
50
50
  raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
51
51
 
52
- authority = accept_authority_for(store)
52
+ authority = accept_role_for(store)
53
53
  ops = store.as(authority)
54
54
  touched = []
55
55
  store.manifest.resolver.enumerate.each do |row|
@@ -87,13 +87,13 @@ module Textus
87
87
  raise UsageError.new("schema not found: #{name}")
88
88
  end
89
89
 
90
- def self.accept_authority_for(store)
91
- authority = store.manifest.policy.roles_with_kind(:accept_authority).first
90
+ def self.accept_role_for(store)
91
+ authority = store.manifest.policy.roles_with_capability("author").first
92
92
  return authority if authority
93
93
 
94
94
  raise UsageError.new(
95
- "schema migrate requires a role with kind :accept_authority in the manifest; " \
96
- "none declared (add e.g. `- { name: owner, kind: accept_authority }` to roles:)",
95
+ "schema migrate requires a role holding the 'author' capability; " \
96
+ "none declared (add e.g. `- { name: owner, can: [author] }` to roles:)",
97
97
  )
98
98
  end
99
99
  end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.29.0"
2
+ VERSION = "0.35.1"
3
3
  PROTOCOL = "textus/3"
4
4
  end
@@ -1,37 +1,30 @@
1
- require_relative "authority_gate"
2
-
3
1
  module Textus
4
2
  module Write
5
3
  class Accept
6
- include AuthorityGate
7
-
8
4
  def initialize(container:, call:)
9
- @container = container
10
- @call = call
11
- @manifest = container.manifest
12
- @schemas = container.schemas
13
- @events = container.events
5
+ @container = container
6
+ @call = call
7
+ @manifest = container.manifest
8
+ @schemas = container.schemas
9
+ @events = container.events
14
10
  end
15
11
 
16
12
  def call(pending_key)
17
- assert_accept_authority!("accept")
18
-
19
- env = Textus::Read::Get.new(
20
- container: @container, call: @call,
21
- ).call(pending_key)
13
+ env = Textus::Read::Get.new(container: @container, call: @call).call(pending_key)
22
14
  proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
23
15
  target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
24
16
  action = proposal["action"] || "put"
25
17
 
26
- evaluate_promotion!(env, target)
18
+ guard.for(:accept, target).check!(
19
+ Textus::Domain::Policy::Evaluation.new(
20
+ actor: @call.role, transition: :accept, origin: pending_key,
21
+ target: target, envelope: env, snapshot: @manifest
22
+ ),
23
+ )
27
24
 
28
25
  case action
29
26
  when "put"
30
- # Nested proposal "frontmatter" the meta to write to the accepted
31
- # target. Not related to the removed intake-handler legacy bridge.
32
- target_meta = env.meta["frontmatter"] || {}
33
- target_body = env.body
34
- put_op.call(target, meta: target_meta, body: target_body)
27
+ put_op.call(target, meta: env.meta["frontmatter"] || {}, body: env.body)
35
28
  when "delete"
36
29
  delete_op.call(target)
37
30
  else
@@ -39,48 +32,19 @@ module Textus
39
32
  end
40
33
 
41
34
  delete_op.call(pending_key)
42
-
43
- @events.publish(:proposal_accepted,
44
- ctx: hook_context,
45
- key: pending_key,
46
- target_key: target)
47
-
35
+ @events.publish(:proposal_accepted, ctx: hook_context, key: pending_key, target_key: target)
48
36
  { "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
49
37
  end
50
38
 
51
39
  private
52
40
 
53
- def hook_context
54
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
55
- end
56
-
57
- def put_op
58
- @put_op ||= Textus::Write::Put.new(
59
- container: @container, call: @call,
60
- )
41
+ def guard
42
+ @guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
61
43
  end
62
44
 
63
- def delete_op
64
- @delete_op ||= Textus::Write::Delete.new(
65
- container: @container, call: @call,
66
- )
67
- end
68
-
69
- def evaluate_promotion!(env, target_key)
70
- rules = @manifest.rules.for(target_key)
71
- promote = rules.promote
72
- return if promote.nil? || promote.requires.empty?
73
-
74
- policy = Textus::Domain::Policy::Promotion.from_names(promote.requires)
75
- result = policy.evaluate(
76
- entry: env, schemas: @schemas, manifest: @manifest, role: @call.role,
77
- )
78
- return if result.ok?
79
-
80
- raise ProposalError.new(
81
- "promotion gate failed: #{result.reasons.join("; ")}",
82
- )
83
- end
45
+ def hook_context = @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
46
+ def put_op = @put_op ||= Textus::Write::Put.new(container: @container, call: @call)
47
+ def delete_op = @delete_op ||= Textus::Write::Delete.new(container: @container, call: @call)
84
48
  end
85
49
  end
86
50
  end
@@ -5,7 +5,6 @@ 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
 
@@ -13,7 +12,7 @@ module Textus
13
12
  Textus::Manifest::Data.validate_key!(key)
14
13
  mentry = @manifest.resolver.resolve(key).entry
15
14
 
16
- @authorizer.authorize_write!(mentry, role: @call.role)
15
+ guard_for(:delete, key, if_etag: if_etag).check!(eval_for(:delete, target_key: key))
17
16
 
18
17
  writer.delete(key, mentry: mentry, if_etag: if_etag)
19
18
 
@@ -28,26 +27,25 @@ module Textus
28
27
 
29
28
  private
30
29
 
31
- def hook_context
32
- @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
30
+ def guard_for(transition, key, if_etag: nil)
31
+ Textus::Domain::Policy::GuardFactory.new(
32
+ manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
33
+ ).for(transition, key)
33
34
  end
34
35
 
35
- def writer
36
- @writer ||= Textus::Envelope::IO::Writer.new(
37
- file_store: @container.file_store,
38
- manifest: @container.manifest,
39
- schemas: @container.schemas,
40
- audit_log: @container.audit_log,
41
- call: @call,
42
- reader: reader,
36
+ def eval_for(transition, target_key:, envelope: nil)
37
+ Textus::Domain::Policy::Evaluation.new(
38
+ actor: @call.role, transition: transition, origin: nil,
39
+ target: target_key, envelope: envelope, snapshot: @manifest
43
40
  )
44
41
  end
45
42
 
46
- def reader
47
- @reader ||= Textus::Envelope::IO::Reader.new(
48
- file_store: @container.file_store,
49
- manifest: @container.manifest,
50
- )
43
+ def hook_context
44
+ @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
45
+ end
46
+
47
+ def writer
48
+ @writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
51
49
  end
52
50
  end
53
51
  end
@@ -1,28 +1,28 @@
1
1
  module Textus
2
2
  module Write
3
- class RefreshAll
3
+ class FetchAll
4
4
  def initialize(container:, call:)
5
5
  @container = container
6
6
  @call = call
7
7
  end
8
8
 
9
9
  def call(prefix: nil, zone: nil)
10
- worker = Textus::Write::RefreshWorker.new(
10
+ worker = Textus::Write::FetchWorker.new(
11
11
  container: @container, call: @call,
12
12
  )
13
13
 
14
14
  stale_rows = Textus::Read::Stale.new(container: @container, call: @call).call(prefix: prefix, zone: zone)
15
- refreshed = []
15
+ fetched = []
16
16
  failed = []
17
17
  skipped = []
18
18
 
19
19
  stale_rows.each do |row|
20
20
  key = row["key"] || row[:key]
21
21
  reason = row["reason"] || row[:reason]
22
- if reason.to_s.match?(/ttl exceeded|never refreshed/)
22
+ if reason.to_s.match?(/ttl exceeded|never fetched/)
23
23
  begin
24
24
  worker.run(key)
25
- refreshed << key
25
+ fetched << key
26
26
  rescue Textus::Error => e
27
27
  failed << { "key" => key, "error" => e.message }
28
28
  end
@@ -34,7 +34,7 @@ module Textus
34
34
  {
35
35
  "protocol" => Textus::PROTOCOL,
36
36
  "ok" => failed.empty?,
37
- "refreshed" => refreshed,
37
+ "fetched" => fetched,
38
38
  "failed" => failed,
39
39
  "skipped" => skipped,
40
40
  }
@@ -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