textus 0.22.0 → 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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +148 -45
- data/CHANGELOG.md +102 -0
- data/README.md +1 -1
- data/SPEC.md +12 -12
- data/docs/conventions.md +10 -0
- data/lib/textus/application/caps.rb +49 -0
- data/lib/textus/application/context.rb +2 -2
- data/lib/textus/application/envelope/reader.rb +44 -0
- data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
- data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
- data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
- data/lib/textus/application/maintenance/migrate.rb +59 -0
- data/lib/textus/application/maintenance/rule_lint.rb +65 -0
- data/lib/textus/application/maintenance/zone_mv.rb +60 -0
- data/lib/textus/application/maintenance.rb +17 -0
- data/lib/textus/application/projection.rb +12 -10
- data/lib/textus/application/read/audit.rb +106 -0
- data/lib/textus/application/read/blame.rb +91 -0
- data/lib/textus/application/read/deps.rb +34 -0
- data/lib/textus/application/read/freshness.rb +110 -0
- data/lib/textus/application/read/get.rb +75 -0
- data/lib/textus/application/read/get_or_refresh.rb +63 -0
- data/lib/textus/application/read/list.rb +25 -0
- data/lib/textus/application/read/policy_explain.rb +47 -0
- data/lib/textus/application/read/published.rb +25 -0
- data/lib/textus/application/read/pulse.rb +101 -0
- data/lib/textus/application/read/rdeps.rb +35 -0
- data/lib/textus/application/read/schema_envelope.rb +26 -0
- data/lib/textus/application/read/stale.rb +23 -0
- data/lib/textus/application/read/uid.rb +30 -0
- data/lib/textus/application/read/validate_all.rb +32 -0
- data/lib/textus/application/{reads → read}/validator.rb +2 -2
- data/lib/textus/application/read/where.rb +26 -0
- data/lib/textus/application/use_case.rb +22 -0
- data/lib/textus/application/write/accept.rb +102 -0
- data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
- data/lib/textus/application/write/delete.rb +45 -0
- data/lib/textus/application/{writes → write}/materializer.rb +14 -15
- data/lib/textus/application/write/mv.rb +118 -0
- data/lib/textus/application/write/publish.rb +96 -0
- data/lib/textus/application/write/put.rb +49 -0
- data/lib/textus/application/write/refresh_all.rb +63 -0
- data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
- data/lib/textus/application/write/refresh_worker.rb +134 -0
- data/lib/textus/application/write/reject.rb +62 -0
- data/lib/textus/boot.rb +27 -29
- data/lib/textus/builder/pipeline.rb +3 -3
- data/lib/textus/cli/group/mcp.rb +9 -0
- data/lib/textus/cli/group/zone.rb +9 -0
- data/lib/textus/cli/verb/accept.rb +1 -1
- data/lib/textus/cli/verb/audit.rb +2 -2
- data/lib/textus/cli/verb/blame.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/delete.rb +1 -1
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -1
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +11 -14
- data/lib/textus/cli/verb/key_delete.rb +24 -0
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +17 -0
- data/lib/textus/cli/verb/migrate.rb +18 -0
- data/lib/textus/cli/verb/mv.rb +11 -3
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/pulse.rb +1 -1
- data/lib/textus/cli/verb/put.rb +8 -6
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +1 -1
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -1
- data/lib/textus/cli/verb/rule_lint.rb +18 -0
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb/zone_mv.rb +19 -0
- data/lib/textus/cli/verb.rb +4 -4
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
- data/lib/textus/doctor/check/hooks.rb +4 -3
- data/lib/textus/doctor/check/illegal_keys.rb +2 -2
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- data/lib/textus/doctor/check/manifest_files.rb +2 -2
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/refresh_locks.rb +2 -2
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/schemas.rb +2 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +2 -2
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +5 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +4 -4
- data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
- data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
- data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
- data/lib/textus/domain/staleness/generator_check.rb +2 -2
- data/lib/textus/domain/staleness/intake_check.rb +2 -2
- data/lib/textus/domain/staleness.rb +1 -1
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +13 -13
- data/lib/textus/hooks/error_log.rb +32 -0
- data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
- data/lib/textus/hooks/loader.rb +29 -3
- data/lib/textus/hooks/rpc_registry.rb +77 -0
- data/lib/textus/infra/audit_subscriber.rb +6 -7
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/key/path.rb +7 -3
- data/lib/textus/manifest/data.rb +78 -0
- data/lib/textus/manifest/entry/base.rb +4 -4
- data/lib/textus/manifest/entry/derived.rb +4 -5
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/policy.rb +48 -0
- data/lib/textus/manifest/resolver.rb +14 -14
- data/lib/textus/manifest/rules.rb +1 -1
- data/lib/textus/manifest.rb +53 -111
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +127 -0
- data/lib/textus/mcp/session.rb +31 -0
- data/lib/textus/mcp/tool_schemas.rb +71 -0
- data/lib/textus/mcp/tools.rb +129 -0
- data/lib/textus/mcp.rb +6 -0
- data/lib/textus/schema/tools.rb +14 -10
- data/lib/textus/session.rb +84 -0
- data/lib/textus/store.rb +14 -9
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +8 -1
- metadata +61 -36
- data/lib/textus/application/reads/audit.rb +0 -94
- data/lib/textus/application/reads/blame.rb +0 -82
- data/lib/textus/application/reads/deps.rb +0 -26
- data/lib/textus/application/reads/freshness.rb +0 -88
- data/lib/textus/application/reads/get.rb +0 -67
- data/lib/textus/application/reads/get_or_refresh.rb +0 -51
- data/lib/textus/application/reads/list.rb +0 -17
- data/lib/textus/application/reads/policy_explain.rb +0 -39
- data/lib/textus/application/reads/published.rb +0 -17
- data/lib/textus/application/reads/pulse.rb +0 -63
- data/lib/textus/application/reads/rdeps.rb +0 -27
- data/lib/textus/application/reads/schema_envelope.rb +0 -18
- data/lib/textus/application/reads/stale.rb +0 -15
- data/lib/textus/application/reads/uid.rb +0 -23
- data/lib/textus/application/reads/validate_all.rb +0 -24
- data/lib/textus/application/reads/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/delete.rb +0 -33
- data/lib/textus/application/writes/mv.rb +0 -105
- data/lib/textus/application/writes/publish.rb +0 -81
- data/lib/textus/application/writes/put.rb +0 -37
- data/lib/textus/application/writes/reject.rb +0 -50
- data/lib/textus/infra/event_bus.rb +0 -27
- data/lib/textus/operations.rb +0 -176
|
@@ -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,63 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Reads
|
|
4
|
-
# Aggregator over audit + freshness + review + doctor. One round-trip
|
|
5
|
-
# for an agent's per-turn heartbeat. All component reads are existing
|
|
6
|
-
# APIs; pulse is sugar with a stable envelope shape and a monotonic
|
|
7
|
-
# cursor (seq).
|
|
8
|
-
class Pulse
|
|
9
|
-
def initialize(ctx:, manifest:, file_store:, audit_log:, root:, store:)
|
|
10
|
-
@ctx = ctx
|
|
11
|
-
@manifest = manifest
|
|
12
|
-
@file_store = file_store
|
|
13
|
-
@audit_log = audit_log
|
|
14
|
-
@root = root
|
|
15
|
-
@store = store
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(since: 0)
|
|
19
|
-
changed = audit_changes_since(since)
|
|
20
|
-
{
|
|
21
|
-
"cursor" => @audit_log.latest_seq,
|
|
22
|
-
"changed" => changed,
|
|
23
|
-
"stale" => stale_keys,
|
|
24
|
-
"pending_review" => review_keys,
|
|
25
|
-
"doctor" => doctor_summary,
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def audit_changes_since(seq)
|
|
32
|
-
Reads::Audit.new(manifest: @manifest, root: @root, audit_log: @audit_log)
|
|
33
|
-
.call(seq_since: seq)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def stale_keys
|
|
37
|
-
# Freshness rows use symbol keys: { key: "x.y", status: :stale, ... }
|
|
38
|
-
rows = Reads::Freshness.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call
|
|
39
|
-
rows.select { |r| r[:status] == :stale }.map { |r| r[:key] }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def review_keys
|
|
43
|
-
# List constructor takes only manifest:; returns hashes with string keys.
|
|
44
|
-
# Guard: zones is a Hash keyed by name string.
|
|
45
|
-
return [] unless @manifest.zones.key?("review")
|
|
46
|
-
|
|
47
|
-
rows = Reads::List.new(manifest: @manifest).call(zone: "review")
|
|
48
|
-
rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def doctor_summary
|
|
52
|
-
result = Textus::Doctor.run(@store)
|
|
53
|
-
issues = result["issues"] || []
|
|
54
|
-
{
|
|
55
|
-
"ok" => result["ok"],
|
|
56
|
-
"warn" => issues.count { |i| i["level"] == "warning" },
|
|
57
|
-
"fail" => issues.count { |i| i["level"] == "error" },
|
|
58
|
-
}
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
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
|