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
@@ -29,16 +29,16 @@ module Textus
29
29
  "meta" => { "type" => "object" },
30
30
  "body" => { "type" => "string" },
31
31
  }, %w[key meta]),
32
- tool("refresh", "Run an intake refresh for one key. Returns the refresh Outcome.",
32
+ tool("fetch", "Run an intake fetch for one key. Returns the fetch Outcome.",
33
33
  { "key" => { "type" => "string" } }, ["key"]),
34
- tool("refresh_stale", "Refresh all stale intake entries, optionally scoped by zone/prefix.",
34
+ tool("fetch_stale", "Fetch all stale intake entries, optionally scoped by zone/prefix.",
35
35
  {
36
36
  "zone" => { "type" => "string" },
37
37
  "prefix" => { "type" => "string" },
38
38
  }, []),
39
39
  tool("schema", "Return the schema (field shape) for an entry family.",
40
40
  { "family" => { "type" => "string" } }, ["family"]),
41
- tool("rules", "Return effective rules for a key (refresh, promote, ...).",
41
+ tool("rules", "Return effective rules for a key (fetch, promote, ...).",
42
42
  { "key" => { "type" => "string" } }, ["key"]),
43
43
  tool("key_mv_prefix",
44
44
  "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false.",
@@ -64,14 +64,14 @@ module Textus
64
64
  { "uid" => env.uid, "etag" => env.etag, "key" => target }
65
65
  end,
66
66
 
67
- "refresh" => lambda do |s, store, args|
68
- key = args.fetch("key") { raise ToolError.new("refresh: missing key") }
69
- outcome = ops_for(s, store).refresh(key)
67
+ "fetch" => lambda do |s, store, args|
68
+ key = args.fetch("key") { raise ToolError.new("fetch: missing key") }
69
+ outcome = ops_for(s, store).fetch(key)
70
70
  { "outcome" => outcome.class.name.split("::").last.downcase }
71
71
  end,
72
72
 
73
- "refresh_stale" => lambda do |s, store, args|
74
- ops_for(s, store).refresh_all(zone: args["zone"], prefix: args["prefix"])
73
+ "fetch_stale" => lambda do |s, store, args|
74
+ ops_for(s, store).fetch_all(zone: args["zone"], prefix: args["prefix"])
75
75
  end,
76
76
 
77
77
  "schema" => lambda do |_s, store, args|
@@ -83,8 +83,8 @@ module Textus
83
83
  key = args.fetch("key") { raise ToolError.new("rules: missing key") }
84
84
  set = store.manifest.rules.for(key)
85
85
  {
86
- "refresh" => set.refresh&.to_h,
87
- "promote" => set.respond_to?(:promote) ? set.promote&.to_h : nil,
86
+ "fetch" => set.fetch&.to_h,
87
+ "guard" => set.guard,
88
88
  }.compact
89
89
  end,
90
90
 
@@ -33,7 +33,7 @@ module Textus
33
33
  extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
34
34
  extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
35
35
  @audit_log.append(
36
- role: "runner", verb: "event_error", key: key,
36
+ role: "automation", verb: "event_error", key: key,
37
37
  etag_before: nil, etag_after: nil, extras: extras
38
38
  )
39
39
  end
@@ -1,6 +1,6 @@
1
1
  module Textus
2
2
  module Ports
3
- module Refresh
3
+ module Fetch
4
4
  module Detached
5
5
  module_function
6
6
 
@@ -16,14 +16,14 @@ module Textus
16
16
  $stdout.reopen(File::NULL, "w")
17
17
  $stderr.reopen(File::NULL, "w")
18
18
 
19
- lock = Textus::Ports::Refresh::Lock.new(root: store_root, key: key)
19
+ lock = Textus::Ports::Fetch::Lock.new(root: store_root, key: key)
20
20
  exit(0) unless lock.try_acquire
21
21
 
22
22
  begin
23
23
  store = Textus::Store.new(store_root)
24
- store.as("runner").refresh(key)
24
+ store.as("automation").fetch(key)
25
25
  rescue StandardError
26
- # Already logged via :refresh_failed; exit cleanly.
26
+ # Already logged via :fetch_failed; exit cleanly.
27
27
  ensure
28
28
  lock.release
29
29
  exit(0)
@@ -2,7 +2,7 @@ require "fileutils"
2
2
 
3
3
  module Textus
4
4
  module Ports
5
- module Refresh
5
+ module Fetch
6
6
  class Lock
7
7
  def initialize(root:, key:)
8
8
  @root = root
@@ -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,40 +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?,
23
25
  retention: !b.retention.nil?,
24
26
  }
25
27
  end,
26
28
  effective: {
27
- refresh: winners.refresh && {
28
- ttl_seconds: winners.refresh.ttl_seconds,
29
- on_stale: winners.refresh.on_stale,
29
+ fetch: winners.fetch && {
30
+ ttl_seconds: winners.fetch.ttl_seconds,
31
+ on_stale: winners.fetch.on_stale,
30
32
  },
31
33
  handler_allowlist: winners.handler_allowlist&.handlers,
32
- promotion: winners.promote && { requires: winners.promote.requires },
33
34
  retention: winners.retention && {
34
35
  expire_after: winners.retention.expire_after,
35
36
  archive_after: winners.retention.archive_after,
36
37
  },
37
38
  },
39
+ guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
40
+ [transition, factory.for(transition, key).predicates.map(&:name)]
41
+ end,
38
42
  }
39
43
  end
40
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
 
@@ -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)
@@ -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.30.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,6 +27,19 @@ module Textus
28
27
 
29
28
  private
30
29
 
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)
34
+ end
35
+
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
40
+ )
41
+ end
42
+
31
43
  def hook_context
32
44
  @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
33
45
  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
  }