textus 0.20.2 → 0.26.0

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +148 -45
  3. data/CHANGELOG.md +194 -0
  4. data/README.md +8 -5
  5. data/SPEC.md +54 -15
  6. data/docs/conventions.md +10 -0
  7. data/lib/textus/application/caps.rb +49 -0
  8. data/lib/textus/application/context.rb +2 -2
  9. data/lib/textus/application/envelope/reader.rb +44 -0
  10. data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
  11. data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
  12. data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
  13. data/lib/textus/application/maintenance/migrate.rb +59 -0
  14. data/lib/textus/application/maintenance/rule_lint.rb +65 -0
  15. data/lib/textus/application/maintenance/zone_mv.rb +60 -0
  16. data/lib/textus/application/maintenance.rb +17 -0
  17. data/lib/textus/application/projection.rb +12 -10
  18. data/lib/textus/application/read/audit.rb +106 -0
  19. data/lib/textus/application/read/blame.rb +91 -0
  20. data/lib/textus/application/read/deps.rb +34 -0
  21. data/lib/textus/application/read/freshness.rb +110 -0
  22. data/lib/textus/application/read/get.rb +75 -0
  23. data/lib/textus/application/read/get_or_refresh.rb +63 -0
  24. data/lib/textus/application/read/list.rb +25 -0
  25. data/lib/textus/application/read/policy_explain.rb +47 -0
  26. data/lib/textus/application/read/published.rb +25 -0
  27. data/lib/textus/application/read/pulse.rb +101 -0
  28. data/lib/textus/application/read/rdeps.rb +35 -0
  29. data/lib/textus/application/read/schema_envelope.rb +26 -0
  30. data/lib/textus/application/read/stale.rb +23 -0
  31. data/lib/textus/application/read/uid.rb +30 -0
  32. data/lib/textus/application/read/validate_all.rb +32 -0
  33. data/lib/textus/application/{reads → read}/validator.rb +2 -2
  34. data/lib/textus/application/read/where.rb +26 -0
  35. data/lib/textus/application/use_case.rb +22 -0
  36. data/lib/textus/application/write/accept.rb +102 -0
  37. data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
  38. data/lib/textus/application/write/delete.rb +45 -0
  39. data/lib/textus/application/{writes → write}/materializer.rb +14 -15
  40. data/lib/textus/application/write/mv.rb +118 -0
  41. data/lib/textus/application/write/publish.rb +96 -0
  42. data/lib/textus/application/write/put.rb +49 -0
  43. data/lib/textus/application/write/refresh_all.rb +63 -0
  44. data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
  45. data/lib/textus/application/write/refresh_worker.rb +134 -0
  46. data/lib/textus/application/write/reject.rb +62 -0
  47. data/lib/textus/{intro.rb → boot.rb} +49 -29
  48. data/lib/textus/builder/pipeline.rb +5 -5
  49. data/lib/textus/cli/group/mcp.rb +9 -0
  50. data/lib/textus/cli/group/zone.rb +9 -0
  51. data/lib/textus/cli/verb/accept.rb +1 -1
  52. data/lib/textus/cli/verb/audit.rb +4 -2
  53. data/lib/textus/cli/verb/blame.rb +1 -1
  54. data/lib/textus/cli/verb/boot.rb +13 -0
  55. data/lib/textus/cli/verb/build.rb +2 -2
  56. data/lib/textus/cli/verb/delete.rb +1 -1
  57. data/lib/textus/cli/verb/deps.rb +1 -1
  58. data/lib/textus/cli/verb/doctor.rb +1 -1
  59. data/lib/textus/cli/verb/freshness.rb +1 -1
  60. data/lib/textus/cli/verb/get.rb +1 -1
  61. data/lib/textus/cli/verb/hook_run.rb +3 -4
  62. data/lib/textus/cli/verb/hooks.rb +11 -14
  63. data/lib/textus/cli/verb/key_delete.rb +24 -0
  64. data/lib/textus/cli/verb/list.rb +1 -1
  65. data/lib/textus/cli/verb/mcp_serve.rb +17 -0
  66. data/lib/textus/cli/verb/migrate.rb +18 -0
  67. data/lib/textus/cli/verb/mv.rb +11 -3
  68. data/lib/textus/cli/verb/published.rb +1 -1
  69. data/lib/textus/cli/verb/pulse.rb +17 -0
  70. data/lib/textus/cli/verb/put.rb +8 -6
  71. data/lib/textus/cli/verb/rdeps.rb +1 -1
  72. data/lib/textus/cli/verb/refresh.rb +1 -1
  73. data/lib/textus/cli/verb/refresh_stale.rb +1 -1
  74. data/lib/textus/cli/verb/reject.rb +1 -1
  75. data/lib/textus/cli/verb/rule_explain.rb +1 -1
  76. data/lib/textus/cli/verb/rule_lint.rb +18 -0
  77. data/lib/textus/cli/verb/schema.rb +1 -1
  78. data/lib/textus/cli/verb/uid.rb +1 -1
  79. data/lib/textus/cli/verb/where.rb +1 -1
  80. data/lib/textus/cli/verb/zone_mv.rb +19 -0
  81. data/lib/textus/cli/verb.rb +4 -4
  82. data/lib/textus/cli.rb +1 -1
  83. data/lib/textus/doctor/check/audit_log.rb +2 -2
  84. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  85. data/lib/textus/doctor/check/hooks.rb +4 -3
  86. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  87. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  88. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  89. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  90. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  91. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  92. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  93. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  94. data/lib/textus/doctor/check/schemas.rb +2 -2
  95. data/lib/textus/doctor/check/sentinels.rb +2 -2
  96. data/lib/textus/doctor/check/templates.rb +2 -2
  97. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  98. data/lib/textus/doctor/check.rb +5 -3
  99. data/lib/textus/doctor.rb +24 -27
  100. data/lib/textus/domain/authorizer.rb +4 -4
  101. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  102. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  103. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  104. data/lib/textus/domain/staleness/generator_check.rb +2 -2
  105. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  106. data/lib/textus/domain/staleness.rb +1 -1
  107. data/lib/textus/errors.rb +16 -0
  108. data/lib/textus/hooks/builtin.rb +14 -14
  109. data/lib/textus/hooks/context.rb +13 -13
  110. data/lib/textus/hooks/error_log.rb +32 -0
  111. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  112. data/lib/textus/hooks/loader.rb +29 -3
  113. data/lib/textus/hooks/rpc_registry.rb +77 -0
  114. data/lib/textus/infra/audit_log.rb +126 -16
  115. data/lib/textus/infra/audit_subscriber.rb +6 -7
  116. data/lib/textus/infra/refresh/detached.rb +1 -1
  117. data/lib/textus/key/path.rb +7 -3
  118. data/lib/textus/manifest/data.rb +78 -0
  119. data/lib/textus/manifest/entry/base.rb +44 -7
  120. data/lib/textus/manifest/entry/derived.rb +41 -6
  121. data/lib/textus/manifest/entry/intake.rb +15 -3
  122. data/lib/textus/manifest/entry/leaf.rb +6 -5
  123. data/lib/textus/manifest/entry/nested.rb +42 -3
  124. data/lib/textus/manifest/entry/parser.rb +8 -44
  125. data/lib/textus/manifest/entry/validators/events.rb +2 -2
  126. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -4
  127. data/lib/textus/manifest/entry/validators/index_filename.rb +2 -1
  128. data/lib/textus/manifest/entry/validators/inject_boot.rb +19 -0
  129. data/lib/textus/manifest/entry/validators/publish_each.rb +4 -3
  130. data/lib/textus/manifest/entry/validators.rb +1 -1
  131. data/lib/textus/manifest/entry.rb +3 -0
  132. data/lib/textus/manifest/policy.rb +48 -0
  133. data/lib/textus/manifest/resolver.rb +18 -18
  134. data/lib/textus/manifest/rules.rb +1 -1
  135. data/lib/textus/manifest/schema.rb +20 -6
  136. data/lib/textus/manifest.rb +53 -101
  137. data/lib/textus/mcp/errors.rb +32 -0
  138. data/lib/textus/mcp/server.rb +127 -0
  139. data/lib/textus/mcp/session.rb +31 -0
  140. data/lib/textus/mcp/tool_schemas.rb +71 -0
  141. data/lib/textus/mcp/tools.rb +129 -0
  142. data/lib/textus/mcp.rb +6 -0
  143. data/lib/textus/schema/tools.rb +14 -10
  144. data/lib/textus/session.rb +84 -0
  145. data/lib/textus/store.rb +17 -8
  146. data/lib/textus/version.rb +1 -1
  147. data/lib/textus.rb +8 -1
  148. metadata +65 -38
  149. data/lib/textus/application/reads/audit.rb +0 -69
  150. data/lib/textus/application/reads/blame.rb +0 -82
  151. data/lib/textus/application/reads/deps.rb +0 -26
  152. data/lib/textus/application/reads/freshness.rb +0 -88
  153. data/lib/textus/application/reads/get.rb +0 -67
  154. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  155. data/lib/textus/application/reads/list.rb +0 -17
  156. data/lib/textus/application/reads/policy_explain.rb +0 -39
  157. data/lib/textus/application/reads/published.rb +0 -17
  158. data/lib/textus/application/reads/rdeps.rb +0 -27
  159. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  160. data/lib/textus/application/reads/stale.rb +0 -15
  161. data/lib/textus/application/reads/uid.rb +0 -23
  162. data/lib/textus/application/reads/validate_all.rb +0 -24
  163. data/lib/textus/application/reads/where.rb +0 -18
  164. data/lib/textus/application/refresh/all.rb +0 -52
  165. data/lib/textus/application/refresh/worker.rb +0 -116
  166. data/lib/textus/application/writes/accept.rb +0 -89
  167. data/lib/textus/application/writes/delete.rb +0 -33
  168. data/lib/textus/application/writes/mv.rb +0 -105
  169. data/lib/textus/application/writes/publish.rb +0 -162
  170. data/lib/textus/application/writes/put.rb +0 -37
  171. data/lib/textus/application/writes/reject.rb +0 -50
  172. data/lib/textus/cli/verb/intro.rb +0 -13
  173. data/lib/textus/infra/event_bus.rb +0 -27
  174. data/lib/textus/manifest/entry/validators/inject_intro.rb +0 -21
  175. data/lib/textus/operations.rb +0 -169
@@ -1,69 +0,0 @@
1
- require "json"
2
- require "time"
3
-
4
- module Textus
5
- module Application
6
- module Reads
7
- # Queries .textus/audit.log. Filters: key, zone, role, verb, since,
8
- # correlation_id, limit. Reads the log file as JSON-Lines (legacy TSV
9
- # rows produce nil and are skipped).
10
- class Audit
11
- def initialize(manifest:, root:)
12
- @manifest = manifest
13
- @log_path = File.join(root, "audit.log")
14
- end
15
-
16
- # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
17
- def call(key: nil, zone: nil, role: nil, verb: nil, since: nil, correlation_id: nil, limit: nil)
18
- return [] unless File.exist?(@log_path)
19
-
20
- rows = []
21
- File.foreach(@log_path) do |line|
22
- parsed = parse_row(line.chomp)
23
- next unless parsed
24
- next if key && parsed["key"] != key
25
- next if role && parsed["role"] != role
26
- next if verb && parsed["verb"] != verb
27
- next if zone && !key_in_zone?(parsed["key"], zone)
28
- next if since && (parsed["ts"].nil? || Time.parse(parsed["ts"]) < since)
29
- next if correlation_id && parsed.dig("extras", "correlation_id") != correlation_id
30
-
31
- rows << parsed
32
- break if limit && rows.length >= limit
33
- end
34
- rows
35
- end
36
- # rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
37
-
38
- # Accepts ISO8601 ("2026-01-15", "2026-01-15T10:00:00Z") or a relative
39
- # offset matching /\A(\d+)([smhd])\z/. Returns nil for unparseable input.
40
- def self.parse_since(str, now: Time.now.utc)
41
- return nil if str.nil? || str.empty?
42
- return Time.parse(str) if str =~ /\A\d{4}-\d{2}-\d{2}/
43
-
44
- m = str.match(/\A(\d+)([smhd])\z/) or return nil
45
- mult = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }[m[2]]
46
- now - (m[1].to_i * mult)
47
- end
48
-
49
- private
50
-
51
- def parse_row(line)
52
- return nil if line.empty?
53
- return nil unless line.start_with?("{")
54
-
55
- JSON.parse(line)
56
- rescue JSON::ParserError
57
- nil
58
- end
59
-
60
- def key_in_zone?(key, zone)
61
- mentry = @manifest.resolver.resolve(key).entry
62
- mentry && mentry.zone == zone
63
- rescue Textus::Error
64
- false
65
- end
66
- end
67
- end
68
- end
69
- end
@@ -1,82 +0,0 @@
1
- require "open3"
2
-
3
- module Textus
4
- module Application
5
- module Reads
6
- # For one key, joins every audit-log row with the git commit (sha,
7
- # author, date, subject) that introduced the file state at that audit
8
- # row. Falls back to `git => nil` when not in a git repo or when the
9
- # file is untracked.
10
- class Blame
11
- def initialize(manifest:, root:)
12
- @manifest = manifest
13
- @root = root
14
- end
15
-
16
- def call(key:, limit: nil)
17
- audit_rows = Textus::Application::Reads::Audit.new(manifest: @manifest, root: @root).call(key: key, limit: limit)
18
- path = resolve_path(key)
19
- return audit_rows.map { |r| r.merge("git" => nil) } unless git_tracked?(path)
20
-
21
- audit_rows.map { |r| r.merge("git" => git_commit_at(path, timestamp: r["ts"])) }
22
- end
23
-
24
- private
25
-
26
- def resolve_path(key)
27
- res = @manifest.resolver.resolve(key)
28
- mentry = res.entry
29
- path = res.path
30
- # Nested entries resolve to a file under the entry path; leaf entries
31
- # already have a fully-resolved path. Either way `path` is what git
32
- # needs to know about.
33
- path || Textus::Key::Path.resolve(@manifest, mentry)
34
- rescue Textus::Error
35
- nil
36
- end
37
-
38
- def git_tracked?(path)
39
- return false if path.nil?
40
- return false unless File.exist?(path)
41
- return false unless git_repo?
42
-
43
- _out, _err, status = Open3.capture3(
44
- "git", "ls-files", "--error-unmatch", path,
45
- chdir: @root
46
- )
47
- status.success?
48
- rescue Errno::ENOENT
49
- false
50
- end
51
-
52
- def git_repo?
53
- # Walk up from store root to find a .git directory.
54
- dir = @root
55
- loop do
56
- return true if File.directory?(File.join(dir, ".git"))
57
-
58
- parent = File.dirname(dir)
59
- return false if parent == dir
60
-
61
- dir = parent
62
- end
63
- end
64
-
65
- def git_commit_at(path, timestamp:)
66
- args = ["git", "log", "-1"]
67
- args << "--before=#{timestamp}" if timestamp
68
- args += ["--format=%H%x09%an%x09%aI%x09%s", "--", path]
69
- out, _err, status = Open3.capture3(*args, chdir: @root)
70
- return nil unless status.success?
71
-
72
- sha, author, date, subject = out.strip.split("\t", 4)
73
- return nil if sha.nil? || sha.empty?
74
-
75
- { "sha" => sha, "author" => author, "date" => date, "subject" => subject }
76
- rescue Errno::ENOENT
77
- nil
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Deps
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(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
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,88 +0,0 @@
1
- require "time"
2
-
3
- module Textus
4
- module Application
5
- module Reads
6
- # Per-entry freshness report. Walks every entry declared in the manifest,
7
- # consults `rules_for(key)` for a refresh rule, and reports the
8
- # current status. Status is one of :fresh, :stale, :never_refreshed, or
9
- # :no_policy.
10
- class Freshness
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
16
- end
17
-
18
- def call(prefix: nil, zone: nil)
19
- rows = []
20
- @manifest.entries.each do |mentry|
21
- next if prefix && !mentry.key.start_with?(prefix)
22
- next if zone && mentry.zone != zone
23
-
24
- rows << row_for(mentry)
25
- end
26
- rows
27
- end
28
-
29
- private
30
-
31
- def row_for(mentry)
32
- set = @manifest.rules_for(mentry.key)
33
- refresh = set.refresh
34
- envelope = safe_get(mentry.key)
35
- last = envelope&.meta&.dig("last_refreshed_at")
36
-
37
- return base_row(mentry, last).merge(status: :no_policy) if refresh.nil?
38
-
39
- fp = refresh.to_freshness_policy
40
- verdict = @evaluator.call(fp, envelope, now: @ctx.now)
41
- status = if verdict.fresh? then :fresh
42
- elsif last.nil? then :never_refreshed
43
- else :stale
44
- end
45
-
46
- base_row(mentry, last).merge(
47
- ttl_seconds: fp.ttl_seconds,
48
- on_stale: fp.on_stale,
49
- status: status,
50
- next_due_at: next_due(last, fp.ttl_seconds),
51
- )
52
- end
53
-
54
- def base_row(mentry, last)
55
- {
56
- key: mentry.key,
57
- zone: mentry.zone,
58
- last_refreshed_at: last,
59
- age_seconds: last ? (@ctx.now - Time.parse(last)).to_i : nil,
60
- }
61
- end
62
-
63
- # Returns the raw envelope or nil. Nested entries (mentry.key is a
64
- # prefix, not a leaf) and missing files both resolve to nil.
65
- def safe_get(key)
66
- res = @manifest.resolver.resolve(key)
67
- return nil unless @file_store.exists?(res.path)
68
-
69
- raw = @file_store.read(res.path)
70
- parsed = Entry.for_format(res.entry.format).parse(raw, path: res.path)
71
- Envelope.build(
72
- key: key, mentry: res.entry, path: res.path,
73
- meta: parsed["_meta"], body: parsed["body"],
74
- etag: Etag.for_bytes(raw), content: parsed["content"]
75
- )
76
- rescue Textus::Error
77
- nil
78
- end
79
-
80
- def next_due(last, ttl)
81
- return nil if last.nil? || ttl.nil?
82
-
83
- (Time.parse(last) + ttl).utc.iso8601
84
- end
85
- end
86
- end
87
- end
88
- end
@@ -1,67 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- # Pure read: returns the on-disk envelope annotated with a freshness
5
- # verdict. Never triggers refresh; never invokes the orchestrator.
6
- #
7
- # For interactive reads that want refresh-on-stale, use
8
- # `Reads::GetOrRefresh`, which composes this with the orchestrator.
9
- class Get
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
15
- end
16
-
17
- def call(key)
18
- envelope = read_raw_envelope(key)
19
- return nil if envelope.nil?
20
-
21
- policy_set = @manifest.rules_for(key)
22
- refresh_policy = policy_set.refresh
23
- return annotate_fresh(envelope) if refresh_policy.nil?
24
-
25
- policy = refresh_policy.to_freshness_policy
26
- verdict = @evaluator.call(policy, envelope, now: @ctx.now)
27
-
28
- envelope.with(freshness: Textus::Domain::Freshness.build(
29
- stale: verdict.stale?,
30
- reason: verdict.reason,
31
- refreshing: false,
32
- ))
33
- end
34
-
35
- # Strict variant: raises UnknownKey when the entry is missing.
36
- # Used by consumers (e.g. Validator) that need to distinguish absence
37
- # from emptiness.
38
- def get(key)
39
- call(key) || raise(UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)))
40
- end
41
-
42
- private
43
-
44
- def read_raw_envelope(key)
45
- res = @manifest.resolver.resolve(key)
46
- mentry = res.entry
47
- path = res.path
48
- return nil unless @file_store.exists?(path)
49
-
50
- raw = @file_store.read(path)
51
- parsed = Entry.for_format(mentry.format).parse(raw, path: path)
52
- Envelope.build(
53
- key: key, mentry: mentry, path: path,
54
- meta: parsed["_meta"], body: parsed["body"],
55
- etag: Etag.for_bytes(raw), content: parsed["content"]
56
- )
57
- end
58
-
59
- def annotate_fresh(envelope)
60
- envelope.with(freshness: Textus::Domain::Freshness.build(
61
- stale: false, reason: nil, refreshing: false,
62
- ))
63
- end
64
- end
65
- end
66
- end
67
- end
@@ -1,51 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- # Composes pure `Reads::Get` with the refresh orchestrator: runs Get
5
- # to obtain the envelope and freshness verdict, then if the verdict
6
- # is stale and the rule's `on_stale` policy demands action, hands
7
- # off to the orchestrator. Use for interactive reads where the
8
- # caller wants the freshest obtainable envelope.
9
- #
10
- # Pure reads (build, projection, schema tooling) should use
11
- # `Reads::Get` directly; it has no orchestrator dependency.
12
- class GetOrRefresh
13
- def initialize(manifest:, get:, orchestrator:)
14
- @manifest = manifest
15
- @get = get
16
- @orchestrator = orchestrator
17
- end
18
-
19
- def call(key)
20
- envelope = @get.call(key)
21
- return nil if envelope.nil?
22
- return envelope unless envelope.freshness&.stale
23
-
24
- policy_set = @manifest.rules_for(key)
25
- refresh_policy = policy_set.refresh
26
- return envelope if refresh_policy.nil?
27
-
28
- policy = refresh_policy.to_freshness_policy
29
- verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
30
- action = policy.decide(verdict)
31
- outcome = @orchestrator.execute(action, key: key)
32
-
33
- case outcome
34
- when Textus::Domain::Outcome::Skipped
35
- envelope
36
- when Textus::Domain::Outcome::Refreshed
37
- outcome.envelope.with(
38
- freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, refreshing: false),
39
- )
40
- when Textus::Domain::Outcome::Detached
41
- envelope.with(freshness: envelope.freshness.with(refreshing: true))
42
- when Textus::Domain::Outcome::Failed
43
- envelope.with(
44
- freshness: envelope.freshness.with(refresh_error: outcome.error.message),
45
- )
46
- end
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class List
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(prefix: nil, zone: nil)
10
- rows = @manifest.resolver.enumerate(prefix: prefix)
11
- rows = rows.select { |r| r[:manifest_entry].zone == zone } if zone
12
- rows.map { |row| { "key" => row[:key], "zone" => row[:manifest_entry].zone, "path" => row[:path] } }
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,39 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- # For one key, surface every matching policy block along with the
5
- # per-slot effective value (which loses ties win-by-specificity).
6
- class PolicyExplain
7
- def initialize(manifest:)
8
- @manifest = manifest
9
- end
10
-
11
- def call(key:)
12
- policies = @manifest.rules
13
- matching = policies.explain(key)
14
- winners = policies.for(key)
15
-
16
- {
17
- key: key,
18
- matched_blocks: matching.map do |b|
19
- {
20
- match: b.match,
21
- refresh: !b.refresh.nil?,
22
- handler_allowlist: !b.handler_allowlist.nil?,
23
- promote: !b.promote.nil?,
24
- }
25
- end,
26
- effective: {
27
- refresh: winners.refresh && {
28
- ttl_seconds: winners.refresh.ttl_seconds,
29
- on_stale: winners.refresh.on_stale,
30
- },
31
- handler_allowlist: winners.handler_allowlist&.handlers,
32
- promotion: winners.promote && { requires: winners.promote.requires },
33
- },
34
- }
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Published
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call
10
- @manifest.entries.reject { |e| e.publish_to.empty? }.map do |e|
11
- { "key" => e.key, "publish_to" => e.publish_to }
12
- end
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,27 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Rdeps
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(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
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class SchemaEnvelope
5
- def initialize(manifest:, schemas:)
6
- @manifest = manifest
7
- @schemas = schemas
8
- end
9
-
10
- def call(key)
11
- mentry = @manifest.resolver.resolve(key).entry
12
- schema = @schemas.fetch_or_nil(mentry.schema)
13
- { "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Stale
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(prefix: nil, zone: nil)
10
- Textus::Domain::Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,23 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Uid
5
- def initialize(ctx:, manifest:, file_store:)
6
- @ctx = ctx
7
- @manifest = manifest
8
- @file_store = file_store
9
- end
10
-
11
- def call(key)
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)
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class ValidateAll
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
11
- end
12
-
13
- def call
14
- Validator.new(
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) },
19
- ).call
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- module Application
3
- module Reads
4
- class Where
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(key)
10
- res = @manifest.resolver.resolve(key)
11
- mentry = res.entry
12
- path = res.path
13
- { "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,52 +0,0 @@
1
- module Textus
2
- module Application
3
- module Refresh
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
14
-
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
19
- )
20
-
21
- stale_rows = Textus::Application::Reads::Stale.new(manifest: @manifest).call(prefix: prefix, zone: zone)
22
- refreshed = []
23
- failed = []
24
- skipped = []
25
-
26
- stale_rows.each do |row|
27
- key = row["key"] || row[:key]
28
- reason = row["reason"] || row[:reason]
29
- if reason.to_s.match?(/ttl exceeded|never refreshed/)
30
- begin
31
- worker.run(key)
32
- refreshed << key
33
- rescue Textus::Error => e
34
- failed << { "key" => key, "error" => e.message }
35
- end
36
- else
37
- skipped << { "key" => key, "reason" => reason }
38
- end
39
- end
40
-
41
- {
42
- "protocol" => Textus::PROTOCOL,
43
- "ok" => failed.empty?,
44
- "refreshed" => refreshed,
45
- "failed" => failed,
46
- "skipped" => skipped,
47
- }
48
- end
49
- end
50
- end
51
- end
52
- end