textus 0.18.0 → 0.20.2

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +43 -48
  3. data/CHANGELOG.md +238 -0
  4. data/SPEC.md +35 -2
  5. data/lib/textus/application/context.rb +20 -58
  6. data/lib/textus/application/policy/predicates/accept_authority_signed.rb +33 -0
  7. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  8. data/lib/textus/{domain → application}/policy/promotion.rb +18 -6
  9. data/lib/textus/application/projection.rb +91 -0
  10. data/lib/textus/application/reads/audit.rb +4 -4
  11. data/lib/textus/application/reads/blame.rb +9 -8
  12. data/lib/textus/application/reads/deps.rb +14 -3
  13. data/lib/textus/application/reads/freshness.rb +10 -8
  14. data/lib/textus/application/reads/get.rb +10 -8
  15. data/lib/textus/application/reads/get_or_refresh.rb +3 -3
  16. data/lib/textus/application/reads/list.rb +3 -3
  17. data/lib/textus/application/reads/policy_explain.rb +3 -3
  18. data/lib/textus/application/reads/published.rb +5 -3
  19. data/lib/textus/application/reads/rdeps.rb +15 -3
  20. data/lib/textus/application/reads/schema_envelope.rb +5 -4
  21. data/lib/textus/application/reads/stale.rb +3 -3
  22. data/lib/textus/application/reads/uid.rb +11 -3
  23. data/lib/textus/application/reads/validate_all.rb +10 -6
  24. data/lib/textus/application/reads/validator.rb +5 -3
  25. data/lib/textus/application/reads/where.rb +3 -3
  26. data/lib/textus/application/refresh/all.rb +15 -11
  27. data/lib/textus/application/refresh/orchestrator.rb +9 -8
  28. data/lib/textus/application/refresh/worker.rb +56 -32
  29. data/lib/textus/application/writes/accept.rb +43 -16
  30. data/lib/textus/application/writes/authority_gate.rb +26 -0
  31. data/lib/textus/application/writes/delete.rb +13 -10
  32. data/lib/textus/application/writes/envelope_io.rb +64 -4
  33. data/lib/textus/application/writes/materializer.rb +50 -0
  34. data/lib/textus/application/writes/mv.rb +57 -94
  35. data/lib/textus/application/writes/publish.rb +132 -26
  36. data/lib/textus/application/writes/put.rb +15 -14
  37. data/lib/textus/application/writes/reject.rb +25 -12
  38. data/lib/textus/builder/pipeline.rb +21 -15
  39. data/lib/textus/builder/renderer/json.rb +4 -1
  40. data/lib/textus/builder/renderer/markdown.rb +7 -1
  41. data/lib/textus/builder/renderer/yaml.rb +4 -1
  42. data/lib/textus/cli/verb/build.rb +4 -6
  43. data/lib/textus/cli/verb/get.rb +1 -1
  44. data/lib/textus/cli/verb/hook_run.rb +3 -4
  45. data/lib/textus/cli/verb/hooks.rb +5 -5
  46. data/lib/textus/cli/verb/put.rb +2 -3
  47. data/lib/textus/cli/verb/refresh_stale.rb +1 -2
  48. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  49. data/lib/textus/doctor/check/hooks.rb +2 -2
  50. data/lib/textus/doctor/check/illegal_keys.rb +7 -7
  51. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  52. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  53. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  54. data/lib/textus/doctor/check/templates.rb +4 -3
  55. data/lib/textus/doctor.rb +3 -4
  56. data/lib/textus/domain/authorizer.rb +37 -0
  57. data/lib/textus/domain/policy/promote.rb +4 -2
  58. data/lib/textus/domain/policy/refresh.rb +2 -0
  59. data/lib/textus/domain/staleness/generator_check.rb +8 -7
  60. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  61. data/lib/textus/hooks/builtin.rb +6 -6
  62. data/lib/textus/hooks/bus.rb +155 -0
  63. data/lib/textus/hooks/context.rb +38 -0
  64. data/lib/textus/hooks/fire_report.rb +23 -0
  65. data/lib/textus/hooks/loader.rb +3 -3
  66. data/lib/textus/infra/audit_subscriber.rb +4 -4
  67. data/lib/textus/infra/event_bus.rb +3 -3
  68. data/lib/textus/infra/refresh/detached.rb +1 -1
  69. data/lib/textus/init.rb +3 -2
  70. data/lib/textus/intro.rb +51 -27
  71. data/lib/textus/manifest/entry/base.rb +38 -0
  72. data/lib/textus/manifest/entry/derived.rb +25 -0
  73. data/lib/textus/manifest/entry/intake.rb +19 -0
  74. data/lib/textus/manifest/entry/leaf.rb +16 -0
  75. data/lib/textus/manifest/entry/nested.rb +39 -0
  76. data/lib/textus/manifest/entry/parser.rb +58 -31
  77. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  78. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  79. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  80. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  81. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  82. data/lib/textus/manifest/entry.rb +0 -72
  83. data/lib/textus/manifest/resolver.rb +112 -0
  84. data/lib/textus/manifest/role_kinds.rb +21 -0
  85. data/lib/textus/manifest/schema.rb +46 -2
  86. data/lib/textus/manifest.rb +24 -101
  87. data/lib/textus/operations.rb +131 -74
  88. data/lib/textus/schema/tools.rb +10 -3
  89. data/lib/textus/store.rb +6 -6
  90. data/lib/textus/version.rb +1 -1
  91. metadata +18 -14
  92. data/lib/textus/application/writes/build.rb +0 -78
  93. data/lib/textus/cli/verb/key_normalize.rb +0 -19
  94. data/lib/textus/dependencies.rb +0 -23
  95. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  96. data/lib/textus/domain/policy.rb +0 -7
  97. data/lib/textus/hooks/dispatcher.rb +0 -71
  98. data/lib/textus/hooks/registry.rb +0 -85
  99. data/lib/textus/manifest/resolution.rb +0 -5
  100. data/lib/textus/migrate_keys.rb +0 -187
  101. data/lib/textus/projection.rb +0 -89
  102. data/lib/textus/refresh.rb +0 -39
@@ -1,14 +1,15 @@
1
+ require_relative "predicates/schema_valid"
2
+ require_relative "predicates/accept_authority_signed"
3
+
1
4
  module Textus
2
- module Domain
5
+ module Application
3
6
  module Policy
4
- # Promotion evaluates a list of named predicates against a pending-proposal
5
- # entry and returns a Result indicating whether all requirements are met.
6
7
  class Promotion
7
8
  Result = Struct.new(:ok?, :reasons, keyword_init: true)
8
9
 
9
10
  REGISTRY = {
10
11
  "schema_valid" => -> { Predicates::SchemaValid.new },
11
- "human_accept" => -> { Predicates::HumanAccept.new },
12
+ "accept_authority_signed" => -> { Predicates::AcceptAuthoritySigned.new },
12
13
  }.freeze
13
14
 
14
15
  def self.from_names(names)
@@ -31,14 +32,25 @@ module Textus
31
32
  @predicates.map(&:name)
32
33
  end
33
34
 
34
- def evaluate(entry:, store:)
35
+ def evaluate(entry:, schemas:, manifest:, role:)
35
36
  reasons = []
36
37
  @predicates.each do |pred|
37
- ok = pred.call(entry: entry, store: store)
38
+ ok = invoke(pred, entry: entry, schemas: schemas, manifest: manifest, role: role)
38
39
  reasons << "#{pred.name}: #{pred.reason || "predicate failed"}" unless ok
39
40
  end
40
41
  Result.new(ok?: reasons.empty?, reasons: reasons)
41
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
42
54
  end
43
55
  end
44
56
  end
@@ -0,0 +1,91 @@
1
+ require "time"
2
+ require "timeout"
3
+
4
+ module Textus
5
+ module Application
6
+ class Projection
7
+ MAX_LIMIT = 1000
8
+ REDUCER_TIMEOUT_SECONDS = 2
9
+
10
+ # `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
11
+ # semantics: pure read (`ops.get`) for materialization paths;
12
+ # `ops.get_or_refresh` if you want refresh-on-stale.
13
+ # `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
14
+ # `transform_resolver` — a callable `->(name) { callable_or_raise }`.
15
+ # `transform_context` — `Application::Context` handed to the transform reducer.
16
+ def initialize(reader:, spec:, lister:, transform_resolver:, transform_context:)
17
+ @reader = reader
18
+ @spec = spec || {}
19
+ @lister = lister
20
+ @transform_resolver = transform_resolver
21
+ @transform_context = transform_context
22
+ @limit = (@spec["limit"] || MAX_LIMIT).to_i
23
+ raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
24
+ end
25
+
26
+ def run
27
+ keys = collect_keys
28
+ explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
29
+ rows = keys.map do |key|
30
+ env = @reader.call(key)
31
+ row = pluck(env.meta, env.body)
32
+ explicit_pluck ? row : row.merge("_key" => key)
33
+ end
34
+ reduced = apply_reducer(rows)
35
+ # Reducers may return either an Array of rows (legacy / templated builds)
36
+ # or a Hash that becomes the structured-format payload base. In the Hash
37
+ # case, downstream sort/limit/position markers don't apply, and the
38
+ # builder owns `_meta.generated_at` so we don't stamp it here.
39
+ return reduced if reduced.is_a?(Hash)
40
+
41
+ rows = reduced
42
+ rows = sort(rows)
43
+ rows = rows.first(@limit)
44
+ mark_positions(rows)
45
+ { "entries" => rows, "count" => rows.length, "generated_at" => Time.now.utc.iso8601 }
46
+ end
47
+
48
+ private
49
+
50
+ def apply_reducer(rows)
51
+ name = @spec["transform"] or return rows
52
+ callable = @transform_resolver.call(name)
53
+ Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
54
+ callable.call(store: @transform_context, rows: rows, config: @spec["transform_config"] || {})
55
+ end
56
+ rescue Timeout::Error
57
+ raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
58
+ end
59
+
60
+ def collect_keys
61
+ prefixes = Array(@spec["select"])
62
+ prefixes.flat_map { |p| @lister.call(prefix: p).map { |row| row["key"] } }.uniq
63
+ end
64
+
65
+ def pluck(frontmatter, _body)
66
+ fields = @spec["pluck"]
67
+ if fields.nil? || fields == "*"
68
+ frontmatter
69
+ else
70
+ Array(fields).each_with_object({}) { |f, h| h[f] = frontmatter[f] if frontmatter.key?(f) }
71
+ end
72
+ end
73
+
74
+ # Adds `_first`, `_last`, and `_index` markers so templates can emit
75
+ # delimiters (e.g. JSON commas) via {{^_last}},{{/_last}}.
76
+ def mark_positions(rows)
77
+ last_idx = rows.length - 1
78
+ rows.each_with_index do |row, i|
79
+ row["_index"] = i
80
+ row["_first"] = i.zero?
81
+ row["_last"] = (i == last_idx)
82
+ end
83
+ end
84
+
85
+ def sort(rows)
86
+ sb = @spec["sort_by"] or return rows
87
+ rows.sort_by { |r| r[sb].to_s }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -8,9 +8,9 @@ module Textus
8
8
  # correlation_id, limit. Reads the log file as JSON-Lines (legacy TSV
9
9
  # rows produce nil and are skipped).
10
10
  class Audit
11
- def initialize(ctx:)
12
- @ctx = ctx
13
- @log_path = File.join(@ctx.store.root, "audit.log")
11
+ def initialize(manifest:, root:)
12
+ @manifest = manifest
13
+ @log_path = File.join(root, "audit.log")
14
14
  end
15
15
 
16
16
  # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -58,7 +58,7 @@ module Textus
58
58
  end
59
59
 
60
60
  def key_in_zone?(key, zone)
61
- mentry = @ctx.store.manifest.resolve(key).entry
61
+ mentry = @manifest.resolver.resolve(key).entry
62
62
  mentry && mentry.zone == zone
63
63
  rescue Textus::Error
64
64
  false
@@ -8,12 +8,13 @@ module Textus
8
8
  # row. Falls back to `git => nil` when not in a git repo or when the
9
9
  # file is untracked.
10
10
  class Blame
11
- def initialize(ctx:)
12
- @ctx = ctx
11
+ def initialize(manifest:, root:)
12
+ @manifest = manifest
13
+ @root = root
13
14
  end
14
15
 
15
16
  def call(key:, limit: nil)
16
- audit_rows = Textus::Application::Reads::Audit.new(ctx: @ctx).call(key: key, limit: limit)
17
+ audit_rows = Textus::Application::Reads::Audit.new(manifest: @manifest, root: @root).call(key: key, limit: limit)
17
18
  path = resolve_path(key)
18
19
  return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
19
20
 
@@ -23,13 +24,13 @@ module Textus
23
24
  private
24
25
 
25
26
  def resolve_path(key)
26
- res = @ctx.store.manifest.resolve(key)
27
+ res = @manifest.resolver.resolve(key)
27
28
  mentry = res.entry
28
29
  path = res.path
29
30
  # Nested entries resolve to a file under the entry path; leaf entries
30
31
  # already have a fully-resolved path. Either way `path` is what git
31
32
  # needs to know about.
32
- path || Textus::Key::Path.resolve(@ctx.store.manifest, mentry)
33
+ path || Textus::Key::Path.resolve(@manifest, mentry)
33
34
  rescue Textus::Error
34
35
  nil
35
36
  end
@@ -41,7 +42,7 @@ module Textus
41
42
 
42
43
  _out, _err, status = Open3.capture3(
43
44
  "git", "ls-files", "--error-unmatch", path,
44
- chdir: @ctx.store.root
45
+ chdir: @root
45
46
  )
46
47
  status.success?
47
48
  rescue Errno::ENOENT
@@ -50,7 +51,7 @@ module Textus
50
51
 
51
52
  def git_repo?
52
53
  # Walk up from store root to find a .git directory.
53
- dir = @ctx.store.root
54
+ dir = @root
54
55
  loop do
55
56
  return true if File.directory?(File.join(dir, ".git"))
56
57
 
@@ -65,7 +66,7 @@ module Textus
65
66
  args = ["git", "log", "-1"]
66
67
  args << "--before=#{timestamp}" if timestamp
67
68
  args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
68
- out, _err, status = Open3.capture3(*args, chdir: @ctx.store.root)
69
+ out, _err, status = Open3.capture3(*args, chdir: @root)
69
70
  return nil unless status.success?
70
71
 
71
72
  sha, author, date, subject = out.strip.split("\t", 4)
@@ -2,12 +2,23 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Deps
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- Dependencies.deps_of(@ctx.manifest, key)
10
+ entry = @manifest.entries.find { |e| e.key == key } or return []
11
+ return [] unless entry.is_a?(Textus::Manifest::Entry::Derived)
12
+
13
+ src = entry.source
14
+ result = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
15
+ Array(src.select).compact
16
+ elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
17
+ Array(src.sources).compact
18
+ else
19
+ []
20
+ end
21
+ result.uniq
11
22
  end
12
23
  end
13
24
  end
@@ -8,14 +8,16 @@ module Textus
8
8
  # current status. Status is one of :fresh, :stale, :never_refreshed, or
9
9
  # :no_policy.
10
10
  class Freshness
11
- def initialize(ctx:, evaluator: Textus::Domain::Freshness::Evaluator)
12
- @ctx = ctx
13
- @evaluator = evaluator
11
+ def initialize(ctx:, manifest:, file_store:, evaluator: Textus::Domain::Freshness::Evaluator)
12
+ @ctx = ctx
13
+ @manifest = manifest
14
+ @file_store = file_store
15
+ @evaluator = evaluator
14
16
  end
15
17
 
16
18
  def call(prefix: nil, zone: nil)
17
19
  rows = []
18
- @ctx.manifest.entries.each do |mentry|
20
+ @manifest.entries.each do |mentry|
19
21
  next if prefix && !mentry.key.start_with?(prefix)
20
22
  next if zone && mentry.zone != zone
21
23
 
@@ -27,7 +29,7 @@ module Textus
27
29
  private
28
30
 
29
31
  def row_for(mentry)
30
- set = @ctx.manifest.rules_for(mentry.key)
32
+ set = @manifest.rules_for(mentry.key)
31
33
  refresh = set.refresh
32
34
  envelope = safe_get(mentry.key)
33
35
  last = envelope&.meta&.dig("last_refreshed_at")
@@ -61,10 +63,10 @@ module Textus
61
63
  # Returns the raw envelope or nil. Nested entries (mentry.key is a
62
64
  # prefix, not a leaf) and missing files both resolve to nil.
63
65
  def safe_get(key)
64
- res = @ctx.manifest.resolve(key)
65
- return nil unless @ctx.file_store.exists?(res.path)
66
+ res = @manifest.resolver.resolve(key)
67
+ return nil unless @file_store.exists?(res.path)
66
68
 
67
- raw = @ctx.file_store.read(res.path)
69
+ raw = @file_store.read(res.path)
68
70
  parsed = Entry.for_format(res.entry.format).parse(raw, path: res.path)
69
71
  Envelope.build(
70
72
  key: key, mentry: res.entry, path: res.path,
@@ -7,16 +7,18 @@ module Textus
7
7
  # For interactive reads that want refresh-on-stale, use
8
8
  # `Reads::GetOrRefresh`, which composes this with the orchestrator.
9
9
  class Get
10
- def initialize(ctx:, evaluator: Textus::Domain::Freshness::Evaluator)
11
- @ctx = ctx
12
- @evaluator = evaluator
10
+ def initialize(ctx:, manifest:, file_store:, evaluator: Textus::Domain::Freshness::Evaluator)
11
+ @ctx = ctx
12
+ @manifest = manifest
13
+ @file_store = file_store
14
+ @evaluator = evaluator
13
15
  end
14
16
 
15
17
  def call(key)
16
18
  envelope = read_raw_envelope(key)
17
19
  return nil if envelope.nil?
18
20
 
19
- policy_set = @ctx.manifest.rules_for(key)
21
+ policy_set = @manifest.rules_for(key)
20
22
  refresh_policy = policy_set.refresh
21
23
  return annotate_fresh(envelope) if refresh_policy.nil?
22
24
 
@@ -34,18 +36,18 @@ module Textus
34
36
  # Used by consumers (e.g. Validator) that need to distinguish absence
35
37
  # from emptiness.
36
38
  def get(key)
37
- call(key) || raise(UnknownKey.new(key, suggestions: @ctx.manifest.suggestions_for(key)))
39
+ call(key) || raise(UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)))
38
40
  end
39
41
 
40
42
  private
41
43
 
42
44
  def read_raw_envelope(key)
43
- res = @ctx.manifest.resolve(key)
45
+ res = @manifest.resolver.resolve(key)
44
46
  mentry = res.entry
45
47
  path = res.path
46
- return nil unless @ctx.file_store.exists?(path)
48
+ return nil unless @file_store.exists?(path)
47
49
 
48
- raw = @ctx.file_store.read(path)
50
+ raw = @file_store.read(path)
49
51
  parsed = Entry.for_format(mentry.format).parse(raw, path: path)
50
52
  Envelope.build(
51
53
  key: key, mentry: mentry, path: path,
@@ -10,8 +10,8 @@ module Textus
10
10
  # Pure reads (build, projection, schema tooling) should use
11
11
  # `Reads::Get` directly; it has no orchestrator dependency.
12
12
  class GetOrRefresh
13
- def initialize(ctx:, get:, orchestrator:)
14
- @ctx = ctx
13
+ def initialize(manifest:, get:, orchestrator:)
14
+ @manifest = manifest
15
15
  @get = get
16
16
  @orchestrator = orchestrator
17
17
  end
@@ -21,7 +21,7 @@ module Textus
21
21
  return nil if envelope.nil?
22
22
  return envelope unless envelope.freshness&.stale
23
23
 
24
- policy_set = @ctx.store.manifest.rules_for(key)
24
+ policy_set = @manifest.rules_for(key)
25
25
  refresh_policy = policy_set.refresh
26
26
  return envelope if refresh_policy.nil?
27
27
 
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class List
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(prefix: nil, zone: nil)
10
- rows = @ctx.manifest.enumerate(prefix: prefix)
10
+ rows = @manifest.resolver.enumerate(prefix: prefix)
11
11
  rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
12
12
  rows.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
13
13
  end
@@ -4,12 +4,12 @@ module Textus
4
4
  # For one key, surface every matching policy block along with the
5
5
  # per-slot effective value (which loses ties win-by-specificity).
6
6
  class PolicyExplain
7
- def initialize(ctx:)
8
- @ctx = ctx
7
+ def initialize(manifest:)
8
+ @manifest = manifest
9
9
  end
10
10
 
11
11
  def call(key:)
12
- policies = @ctx.store.manifest.rules
12
+ policies = @manifest.rules
13
13
  matching = policies.explain(key)
14
14
  winners = policies.for(key)
15
15
 
@@ -2,12 +2,14 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Published
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call
10
- Dependencies.published_of(@ctx.manifest)
10
+ @manifest.entries.reject { |e| e.publish_to.empty? }.map do |e|
11
+ { "key" => e.key, "publish_to" => e.publish_to }
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -2,12 +2,24 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Rdeps
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- Dependencies.rdeps_of(@ctx.manifest, key)
10
+ @manifest.entries.each_with_object([]) do |e, acc|
11
+ next unless e.is_a?(Textus::Manifest::Entry::Derived)
12
+
13
+ src = e.source
14
+ sources = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
15
+ Array(src.select).compact
16
+ elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
17
+ Array(src.sources).compact
18
+ else
19
+ []
20
+ end
21
+ acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
22
+ end
11
23
  end
12
24
  end
13
25
  end
@@ -2,13 +2,14 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class SchemaEnvelope
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:, schemas:)
6
+ @manifest = manifest
7
+ @schemas = schemas
7
8
  end
8
9
 
9
10
  def call(key)
10
- mentry = @ctx.manifest.resolve(key).entry
11
- schema = @ctx.schemas.fetch_or_nil(mentry.schema)
11
+ mentry = @manifest.resolver.resolve(key).entry
12
+ schema = @schemas.fetch_or_nil(mentry.schema)
12
13
  { "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
13
14
  end
14
15
  end
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Stale
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(prefix: nil, zone: nil)
10
- Textus::Domain::Staleness.new(manifest: @ctx.manifest).call(prefix: prefix, zone: zone)
10
+ Textus::Domain::Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
11
11
  end
12
12
  end
13
13
  end
@@ -2,12 +2,20 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Uid
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(ctx:, manifest:, file_store:)
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @file_store = file_store
7
9
  end
8
10
 
9
11
  def call(key)
10
- Get.new(ctx: @ctx).get(key).uid
12
+ get.get(key).uid
13
+ end
14
+
15
+ private
16
+
17
+ def get
18
+ @get ||= Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store)
11
19
  end
12
20
  end
13
21
  end
@@ -2,16 +2,20 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class ValidateAll
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(ctx:, manifest:, file_store:, schemas:, audit_log:)
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @file_store = file_store
9
+ @schemas = schemas
10
+ @audit_log = audit_log
7
11
  end
8
12
 
9
13
  def call
10
14
  Validator.new(
11
- reader: Get.new(ctx: @ctx),
12
- manifest: @ctx.manifest,
13
- audit_log: @ctx.audit_log,
14
- schema_for: ->(name) { @ctx.schemas.fetch_or_nil(name) },
15
+ reader: Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store),
16
+ manifest: @manifest,
17
+ audit_log: @audit_log,
18
+ schema_for: ->(name) { @schemas.fetch_or_nil(name) },
15
19
  ).call
16
20
  end
17
21
  end
@@ -19,7 +19,7 @@ module Textus
19
19
  private
20
20
 
21
21
  def check_content_violations(violations)
22
- @manifest.enumerate.each do |row|
22
+ @manifest.resolver.enumerate.each do |row|
23
23
  key = row[:key]
24
24
  mentry = row[:manifest_entry]
25
25
  env = fetch_envelope(key, violations) or next
@@ -35,7 +35,7 @@ module Textus
35
35
  end
36
36
 
37
37
  def check_role_authority_violations(violations)
38
- @manifest.enumerate.each do |row|
38
+ @manifest.resolver.enumerate.each do |row|
39
39
  mentry = row[:manifest_entry]
40
40
  next unless mentry.schema
41
41
 
@@ -55,9 +55,11 @@ module Textus
55
55
  last_writer = @audit_log.last_writer_for(key)
56
56
  return if last_writer.nil?
57
57
 
58
+ last_writer_is_authority = @manifest.role_kind(last_writer) == :accept_authority
59
+
58
60
  env.meta.each_key do |field|
59
61
  owner = schema.maintained_by(field)
60
- next if owner.nil? || last_writer == owner || last_writer == "human"
62
+ next if owner.nil? || last_writer == owner || last_writer_is_authority
61
63
 
62
64
  violations << { "key" => key, "code" => "role_authority",
63
65
  "field" => field, "expected" => owner, "last_writer" => last_writer }
@@ -2,12 +2,12 @@ module Textus
2
2
  module Application
3
3
  module Reads
4
4
  class Where
5
- def initialize(ctx:)
6
- @ctx = ctx
5
+ def initialize(manifest:)
6
+ @manifest = manifest
7
7
  end
8
8
 
9
9
  def call(key)
10
- res = @ctx.manifest.resolve(key)
10
+ res = @manifest.resolver.resolve(key)
11
11
  mentry = res.entry
12
12
  path = res.path
13
13
  { "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
@@ -1,20 +1,24 @@
1
1
  module Textus
2
2
  module Application
3
3
  module Refresh
4
- module All
5
- module_function
4
+ class All
5
+ def initialize(ctx:, manifest:, envelope_io:, bus:, store:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @envelope_io = envelope_io
9
+ @bus = bus
10
+ @store = store
11
+ @authorizer = authorizer
12
+ @hook_context = hook_context
13
+ end
6
14
 
7
- def call(ctx, prefix: nil, zone: nil)
8
- envelope_io = Textus::Application::Writes::EnvelopeIO.new(
9
- file_store: ctx.file_store,
10
- manifest: ctx.manifest,
11
- schemas: ctx.schemas,
12
- audit_log: ctx.audit_log,
13
- ctx: ctx,
15
+ def call(prefix: nil, zone: nil)
16
+ worker = Textus::Application::Refresh::Worker.new(
17
+ ctx: @ctx, manifest: @manifest, envelope_io: @envelope_io, bus: @bus,
18
+ store: @store, authorizer: @authorizer, hook_context: @hook_context
14
19
  )
15
- worker = Textus::Application::Refresh::Worker.new(ctx: ctx, envelope_io: envelope_io)
16
20
 
17
- stale_rows = Textus::Application::Reads::Stale.new(ctx: ctx).call(prefix: prefix, zone: zone)
21
+ stale_rows = Textus::Application::Reads::Stale.new(manifest: @manifest).call(prefix: prefix, zone: zone)
18
22
  refreshed = []
19
23
  failed = []
20
24
  skipped = []
@@ -2,11 +2,13 @@ module Textus
2
2
  module Application
3
3
  module Refresh
4
4
  class Orchestrator
5
- def initialize(worker:, store_root:, store: nil, role: "human", detached_spawner: nil)
6
- @worker = worker
7
- @store_root = store_root
8
- @store = store
9
- @role = role
5
+ def initialize(worker:, store_root:, bus: nil, store: nil, ctx: nil, hook_context: nil, detached_spawner: nil) # rubocop:disable Metrics/ParameterLists
6
+ @worker = worker
7
+ @store_root = store_root
8
+ @bus = bus
9
+ @store = store
10
+ @ctx = ctx
11
+ @hook_context = hook_context
10
12
  @detached_spawner = detached_spawner || default_spawner
11
13
  end
12
14
 
@@ -55,10 +57,9 @@ module Textus
55
57
 
56
58
  probe.release
57
59
 
58
- store_view = @store ? Textus::Application::Context.new(store: @store, role: @role) : nil
59
60
  payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
60
- payload[:store] = store_view if store_view
61
- @store&.bus&.publish(:refresh_backgrounded, **payload)
61
+ payload[:ctx] = @hook_context if @hook_context
62
+ @bus&.publish(:refresh_backgrounded, **payload)
62
63
  @detached_spawner.call(store_root: @store_root, key: key)
63
64
  Textus::Domain::Outcome::Detached.new
64
65
  elsif result.is_a?(Textus::Error)