textus 0.26.0 → 0.30.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 +118 -68
- data/CHANGELOG.md +132 -0
- data/README.md +61 -19
- data/SPEC.md +107 -46
- data/docs/conventions.md +4 -4
- data/lib/textus/boot.rb +18 -12
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/verb/audit.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/doctor.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +2 -6
- data/lib/textus/cli/verb/put.rb +5 -14
- data/lib/textus/cli/verb/retain.rb +19 -0
- data/lib/textus/cli/verb/rule_list.rb +1 -1
- data/lib/textus/cli/verb.rb +6 -6
- data/lib/textus/cli.rb +19 -23
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +57 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +10 -8
- data/lib/textus/doctor/check.rb +15 -5
- data/lib/textus/doctor.rb +7 -7
- data/lib/textus/domain/authorizer.rb +2 -2
- data/lib/textus/domain/duration.rb +22 -0
- data/lib/textus/domain/policy/refresh.rb +1 -15
- data/lib/textus/domain/policy/retention.rb +26 -0
- data/lib/textus/domain/retention.rb +44 -0
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +18 -10
- data/lib/textus/domain/staleness.rb +3 -3
- data/lib/textus/{application/envelope → envelope/io}/reader.rb +6 -2
- data/lib/textus/{application/envelope → envelope/io}/writer.rb +19 -11
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/event_bus.rb +8 -20
- data/lib/textus/hooks/rpc_registry.rb +9 -35
- data/lib/textus/hooks/signature.rb +31 -0
- data/lib/textus/init.rb +7 -6
- data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
- data/lib/textus/maintenance/migrate.rb +51 -0
- data/lib/textus/maintenance/rule_lint.rb +56 -0
- data/lib/textus/maintenance/zone_mv.rb +51 -0
- data/lib/textus/maintenance.rb +15 -0
- data/lib/textus/manifest/data.rb +9 -4
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +6 -6
- data/lib/textus/manifest/entry/nested.rb +7 -9
- data/lib/textus/manifest/entry/parser.rb +2 -2
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
- data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
- data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +2 -2
- data/lib/textus/manifest/entry.rb +0 -5
- data/lib/textus/manifest/policy.rb +34 -7
- data/lib/textus/manifest/rules.rb +10 -1
- data/lib/textus/manifest/schema.rb +54 -4
- data/lib/textus/manifest.rb +4 -8
- data/lib/textus/mcp/server.rb +2 -11
- data/lib/textus/mcp/session.rb +13 -20
- data/lib/textus/mcp/tools.rb +2 -2
- data/lib/textus/mcp.rb +1 -1
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
- data/lib/textus/{infra → ports}/build_lock.rb +1 -1
- data/lib/textus/{infra → ports}/clock.rb +1 -1
- data/lib/textus/{infra → ports}/publisher.rb +6 -6
- data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
- data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +67 -0
- data/lib/textus/ports/storage/file_stat.rb +19 -0
- data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
- data/lib/textus/projection.rb +91 -0
- data/lib/textus/read/audit.rb +111 -0
- data/lib/textus/read/blame.rb +81 -0
- data/lib/textus/read/boot.rb +18 -0
- data/lib/textus/read/deps.rb +24 -0
- data/lib/textus/read/doctor.rb +19 -0
- data/lib/textus/read/freshness.rb +101 -0
- data/lib/textus/read/get.rb +66 -0
- data/lib/textus/read/get_or_refresh.rb +69 -0
- data/lib/textus/read/list.rb +15 -0
- data/lib/textus/read/policy_explain.rb +42 -0
- data/lib/textus/read/published.rb +15 -0
- data/lib/textus/read/pulse.rb +89 -0
- data/lib/textus/read/rdeps.rb +25 -0
- data/lib/textus/read/retainable.rb +17 -0
- data/lib/textus/read/schema_envelope.rb +16 -0
- data/lib/textus/read/stale.rb +17 -0
- data/lib/textus/read/uid.rb +20 -0
- data/lib/textus/read/validate_all.rb +22 -0
- data/lib/textus/read/validator.rb +84 -0
- data/lib/textus/read/where.rb +16 -0
- data/lib/textus/role_scope.rb +50 -0
- data/lib/textus/schema/tools.rb +3 -3
- data/lib/textus/store.rb +16 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +86 -0
- data/lib/textus/write/authority_gate.rb +24 -0
- data/lib/textus/write/delete.rb +40 -0
- data/lib/textus/write/intake_fetch.rb +23 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +113 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +45 -0
- data/lib/textus/write/refresh_all.rb +44 -0
- data/lib/textus/write/refresh_orchestrator.rb +102 -0
- data/lib/textus/write/refresh_worker.rb +124 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus/write/retention_sweep.rb +55 -0
- data/lib/textus.rb +1 -2
- metadata +62 -50
- data/lib/textus/application/caps.rb +0 -49
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
- data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
- data/lib/textus/application/maintenance/migrate.rb +0 -59
- data/lib/textus/application/maintenance/rule_lint.rb +0 -65
- data/lib/textus/application/maintenance/zone_mv.rb +0 -60
- data/lib/textus/application/maintenance.rb +0 -17
- data/lib/textus/application/projection.rb +0 -93
- data/lib/textus/application/read/audit.rb +0 -106
- data/lib/textus/application/read/blame.rb +0 -91
- data/lib/textus/application/read/deps.rb +0 -34
- data/lib/textus/application/read/freshness.rb +0 -110
- data/lib/textus/application/read/get.rb +0 -75
- data/lib/textus/application/read/get_or_refresh.rb +0 -63
- data/lib/textus/application/read/list.rb +0 -25
- data/lib/textus/application/read/policy_explain.rb +0 -47
- data/lib/textus/application/read/published.rb +0 -25
- data/lib/textus/application/read/pulse.rb +0 -101
- data/lib/textus/application/read/rdeps.rb +0 -35
- data/lib/textus/application/read/schema_envelope.rb +0 -26
- data/lib/textus/application/read/stale.rb +0 -23
- data/lib/textus/application/read/uid.rb +0 -30
- data/lib/textus/application/read/validate_all.rb +0 -32
- data/lib/textus/application/read/validator.rb +0 -86
- data/lib/textus/application/read/where.rb +0 -26
- data/lib/textus/application/use_case.rb +0 -22
- data/lib/textus/application/write/accept.rb +0 -102
- data/lib/textus/application/write/authority_gate.rb +0 -26
- data/lib/textus/application/write/delete.rb +0 -45
- data/lib/textus/application/write/materializer.rb +0 -49
- data/lib/textus/application/write/mv.rb +0 -118
- data/lib/textus/application/write/publish.rb +0 -96
- data/lib/textus/application/write/put.rb +0 -49
- data/lib/textus/application/write/refresh_all.rb +0 -63
- data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
- data/lib/textus/application/write/refresh_worker.rb +0 -134
- data/lib/textus/application/write/reject.rb +0 -62
- data/lib/textus/session.rb +0 -84
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
require "digest"
|
|
2
|
-
require "time"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Application
|
|
6
|
-
module Read
|
|
7
|
-
# Aggregator over audit + freshness + review + doctor. One round-trip
|
|
8
|
-
# for an agent's per-turn heartbeat. All component reads are existing
|
|
9
|
-
# APIs; pulse is sugar with a stable envelope shape and a monotonic
|
|
10
|
-
# cursor (seq).
|
|
11
|
-
module Pulse
|
|
12
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
13
|
-
Impl.new(ctx: ctx, caps: caps, session: session).call(*, **)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
class Impl
|
|
17
|
-
def initialize(ctx:, caps:, session:)
|
|
18
|
-
@ctx = ctx
|
|
19
|
-
@caps = caps
|
|
20
|
-
@manifest = caps.manifest
|
|
21
|
-
@file_store = caps.file_store
|
|
22
|
-
@audit_log = caps.audit_log
|
|
23
|
-
@root = caps.root
|
|
24
|
-
@events = caps.events
|
|
25
|
-
@session = session
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(since: 0)
|
|
29
|
-
freshness_rows = freshness.call
|
|
30
|
-
{
|
|
31
|
-
"cursor" => @audit_log.latest_seq,
|
|
32
|
-
"changed" => audit_changes_since(since),
|
|
33
|
-
"stale" => freshness_rows.select { |r| r[:status] == :stale }.map { |r| r[:key] },
|
|
34
|
-
"pending_review" => review_keys,
|
|
35
|
-
"doctor" => doctor_summary,
|
|
36
|
-
"manifest_etag" => manifest_etag,
|
|
37
|
-
"next_due_at" => soonest_due(freshness_rows),
|
|
38
|
-
"hook_errors" => hook_errors_since(since),
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def audit_changes_since(seq)
|
|
45
|
-
Read::Audit::Impl.new(caps: @caps).call(seq_since: seq)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def freshness
|
|
49
|
-
@freshness ||= Read::Freshness::Impl.new(ctx: @ctx, caps: @caps)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def soonest_due(rows)
|
|
53
|
-
times = rows.map { |r| r[:next_due_at] }.compact.map { |t| Time.parse(t) }
|
|
54
|
-
return nil if times.empty?
|
|
55
|
-
|
|
56
|
-
times.min.utc.iso8601
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def review_keys
|
|
60
|
-
# List constructor takes only manifest:; returns hashes with string keys.
|
|
61
|
-
# Guard: zones is a Hash keyed by name string.
|
|
62
|
-
return [] unless @manifest.data.zones.key?("review")
|
|
63
|
-
|
|
64
|
-
rows = Read::List::Impl.new(caps: @caps).call(zone: "review")
|
|
65
|
-
rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def doctor_summary
|
|
69
|
-
result = Textus::Doctor.run(@session)
|
|
70
|
-
issues = result["issues"] || []
|
|
71
|
-
{
|
|
72
|
-
"ok" => result["ok"],
|
|
73
|
-
"warn" => issues.count { |i| i["level"] == "warning" },
|
|
74
|
-
"fail" => issues.count { |i| i["level"] == "error" },
|
|
75
|
-
}
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def manifest_etag
|
|
79
|
-
Digest::SHA256.hexdigest(File.read(File.join(@root, "manifest.yaml")))
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def hook_errors_since(seq)
|
|
83
|
-
@events.error_log.since(seq).map do |r|
|
|
84
|
-
{
|
|
85
|
-
"seq" => r[:seq],
|
|
86
|
-
"event" => r[:event].to_s,
|
|
87
|
-
"hook" => r[:hook].to_s,
|
|
88
|
-
"key" => r[:key],
|
|
89
|
-
"error_class" => r[:error_class],
|
|
90
|
-
"error_message" => r[:error_message],
|
|
91
|
-
"at" => r[:at],
|
|
92
|
-
}
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
Textus::Application::UseCase.register(:pulse, Textus::Application::Read::Pulse, caps: :read)
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module Rdeps
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(caps:)
|
|
11
|
-
@manifest = caps.manifest
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def call(key)
|
|
15
|
-
@manifest.data.entries.each_with_object([]) do |e, acc|
|
|
16
|
-
next unless e.is_a?(Textus::Manifest::Entry::Derived)
|
|
17
|
-
|
|
18
|
-
src = e.source
|
|
19
|
-
sources = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
|
|
20
|
-
Array(src.select).compact
|
|
21
|
-
elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
|
|
22
|
-
Array(src.sources).compact
|
|
23
|
-
else
|
|
24
|
-
[]
|
|
25
|
-
end
|
|
26
|
-
acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
Textus::Application::UseCase.register(:rdeps, Textus::Application::Read::Rdeps, caps: :read)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module SchemaEnvelope
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(caps:)
|
|
11
|
-
@manifest = caps.manifest
|
|
12
|
-
@schemas = caps.schemas
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(key)
|
|
16
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
17
|
-
schema = @schemas.fetch_or_nil(mentry.schema)
|
|
18
|
-
{ "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
Textus::Application::UseCase.register(:schema_envelope, Textus::Application::Read::SchemaEnvelope, caps: :read)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module Stale
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(caps:)
|
|
11
|
-
@manifest = caps.manifest
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def call(prefix: nil, zone: nil)
|
|
15
|
-
Textus::Domain::Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
Textus::Application::UseCase.register(:stale, Textus::Application::Read::Stale, caps: :read)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module Uid
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(ctx: ctx, caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(ctx:, caps:)
|
|
11
|
-
@ctx = ctx
|
|
12
|
-
@caps = caps
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(key)
|
|
16
|
-
get.get(key).uid
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def get
|
|
22
|
-
@get ||= Get::Impl.new(ctx: @ctx, caps: @caps)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
Textus::Application::UseCase.register(:uid, Textus::Application::Read::Uid, caps: :read)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module ValidateAll
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(ctx: ctx, caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(ctx:, caps:)
|
|
11
|
-
@ctx = ctx
|
|
12
|
-
@caps = caps
|
|
13
|
-
@manifest = caps.manifest
|
|
14
|
-
@schemas = caps.schemas
|
|
15
|
-
@audit_log = caps.audit_log
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call
|
|
19
|
-
Validator.new(
|
|
20
|
-
reader: Get::Impl.new(ctx: @ctx, caps: @caps),
|
|
21
|
-
manifest: @manifest,
|
|
22
|
-
audit_log: @audit_log,
|
|
23
|
-
schema_for: ->(name) { @schemas.fetch_or_nil(name) },
|
|
24
|
-
).call
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
Textus::Application::UseCase.register(:validate_all, Textus::Application::Read::ValidateAll, caps: :read)
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
class Validator
|
|
5
|
-
def initialize(reader:, manifest:, audit_log:, schema_for:)
|
|
6
|
-
@reader = reader
|
|
7
|
-
@manifest = manifest
|
|
8
|
-
@audit_log = audit_log
|
|
9
|
-
@schema_for = schema_for
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call
|
|
13
|
-
violations = []
|
|
14
|
-
check_content_violations(violations)
|
|
15
|
-
check_role_authority_violations(violations)
|
|
16
|
-
{ "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def check_content_violations(violations)
|
|
22
|
-
@manifest.resolver.enumerate.each do |row|
|
|
23
|
-
key = row[:key]
|
|
24
|
-
mentry = row[:manifest_entry]
|
|
25
|
-
env = fetch_envelope(key, violations) or next
|
|
26
|
-
schema = mentry.schema && @schema_for.call(mentry.schema)
|
|
27
|
-
next unless schema
|
|
28
|
-
|
|
29
|
-
begin
|
|
30
|
-
validate_schema!(schema, env, mentry.format)
|
|
31
|
-
rescue Textus::Error => e
|
|
32
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def check_role_authority_violations(violations)
|
|
38
|
-
@manifest.resolver.enumerate.each do |row|
|
|
39
|
-
mentry = row[:manifest_entry]
|
|
40
|
-
next unless mentry.schema
|
|
41
|
-
|
|
42
|
-
schema = @schema_for.call(mentry.schema)
|
|
43
|
-
next unless schema
|
|
44
|
-
|
|
45
|
-
env = begin
|
|
46
|
-
@reader.get(row[:key])
|
|
47
|
-
rescue StandardError
|
|
48
|
-
next
|
|
49
|
-
end
|
|
50
|
-
append_authority_violations(violations, row[:key], env, schema)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def append_authority_violations(violations, key, env, schema)
|
|
55
|
-
last_writer = @audit_log.last_writer_for(key)
|
|
56
|
-
return if last_writer.nil?
|
|
57
|
-
|
|
58
|
-
last_writer_is_authority = @manifest.policy.role_kind(last_writer) == :accept_authority
|
|
59
|
-
|
|
60
|
-
env.meta.each_key do |field|
|
|
61
|
-
owner = schema.maintained_by(field)
|
|
62
|
-
next if owner.nil? || last_writer == owner || last_writer_is_authority
|
|
63
|
-
|
|
64
|
-
violations << { "key" => key, "code" => "role_authority",
|
|
65
|
-
"field" => field, "expected" => owner, "last_writer" => last_writer }
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def fetch_envelope(key, violations)
|
|
70
|
-
@reader.get(key)
|
|
71
|
-
rescue Textus::Error => e
|
|
72
|
-
violations << { "key" => key, "code" => e.code, "message" => e.message }
|
|
73
|
-
nil
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def validate_schema!(schema, envelope, format)
|
|
77
|
-
payload = case format
|
|
78
|
-
when "json", "yaml" then envelope.content || {}
|
|
79
|
-
else envelope.meta || {}
|
|
80
|
-
end
|
|
81
|
-
schema.validate!(payload)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Read
|
|
4
|
-
module Where
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
6
|
-
Impl.new(caps: caps).call(*, **)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
class Impl
|
|
10
|
-
def initialize(caps:)
|
|
11
|
-
@manifest = caps.manifest
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def call(key)
|
|
15
|
-
res = @manifest.resolver.resolve(key)
|
|
16
|
-
mentry = res.entry
|
|
17
|
-
path = res.path
|
|
18
|
-
{ "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
Textus::Application::UseCase.register(:where, Textus::Application::Read::Where, caps: :read)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
# Registry mapping verb symbols to use-case modules. Each entry says
|
|
4
|
-
# which caps slice the use case needs (:read or :write); Session
|
|
5
|
-
# uses this to define one method per verb.
|
|
6
|
-
module UseCase
|
|
7
|
-
Entry = Data.define(:verb, :mod, :caps_kind)
|
|
8
|
-
|
|
9
|
-
@entries = []
|
|
10
|
-
|
|
11
|
-
class << self
|
|
12
|
-
attr_reader :entries
|
|
13
|
-
|
|
14
|
-
def register(verb, mod, caps:)
|
|
15
|
-
@entries << Entry.new(verb: verb.to_sym, mod: mod, caps_kind: caps.to_sym)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def each(&) = @entries.each(&)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
require_relative "authority_gate"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
module Write
|
|
6
|
-
module Accept
|
|
7
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
8
|
-
Impl.new(
|
|
9
|
-
ctx: ctx, caps: caps,
|
|
10
|
-
writer: session.envelope_writer,
|
|
11
|
-
hook_context: session.hook_context
|
|
12
|
-
).call(*, **)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class Impl
|
|
16
|
-
include AuthorityGate
|
|
17
|
-
|
|
18
|
-
def initialize(ctx:, caps:, writer:, hook_context:)
|
|
19
|
-
@ctx = ctx
|
|
20
|
-
@caps = caps
|
|
21
|
-
@manifest = caps.manifest
|
|
22
|
-
@file_store = caps.file_store
|
|
23
|
-
@schemas = caps.schemas
|
|
24
|
-
@writer = writer
|
|
25
|
-
@events = caps.events
|
|
26
|
-
@authorizer = caps.authorizer
|
|
27
|
-
@hook_context = hook_context
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def call(pending_key)
|
|
31
|
-
assert_accept_authority!("accept")
|
|
32
|
-
|
|
33
|
-
env = Textus::Application::Read::Get::Impl.new(
|
|
34
|
-
ctx: @ctx, caps: @caps,
|
|
35
|
-
).call(pending_key)
|
|
36
|
-
proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
37
|
-
target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
38
|
-
action = proposal["action"] || "put"
|
|
39
|
-
|
|
40
|
-
evaluate_promotion!(env, target)
|
|
41
|
-
|
|
42
|
-
case action
|
|
43
|
-
when "put"
|
|
44
|
-
# Nested proposal "frontmatter" — the meta to write to the accepted
|
|
45
|
-
# target. Not related to the removed intake-handler legacy bridge.
|
|
46
|
-
target_meta = env.meta["frontmatter"] || {}
|
|
47
|
-
target_body = env.body
|
|
48
|
-
put_op.call(target, meta: target_meta, body: target_body)
|
|
49
|
-
when "delete"
|
|
50
|
-
delete_op.call(target)
|
|
51
|
-
else
|
|
52
|
-
raise ProposalError.new("unknown action: #{action}")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
delete_op.call(pending_key)
|
|
56
|
-
|
|
57
|
-
@events.publish(:proposal_accepted,
|
|
58
|
-
ctx: @hook_context,
|
|
59
|
-
key: pending_key,
|
|
60
|
-
target_key: target)
|
|
61
|
-
|
|
62
|
-
{ "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def put_op
|
|
68
|
-
@put_op ||= Textus::Application::Write::Put::Impl.new(
|
|
69
|
-
ctx: @ctx, caps: @caps, writer: @writer,
|
|
70
|
-
hook_context: @hook_context
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def delete_op
|
|
75
|
-
@delete_op ||= Textus::Application::Write::Delete::Impl.new(
|
|
76
|
-
ctx: @ctx, caps: @caps, writer: @writer,
|
|
77
|
-
hook_context: @hook_context
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def evaluate_promotion!(env, target_key)
|
|
82
|
-
rules = @manifest.rules.for(target_key)
|
|
83
|
-
promote = rules.promote
|
|
84
|
-
return if promote.nil? || promote.requires.empty?
|
|
85
|
-
|
|
86
|
-
policy = Textus::Domain::Policy::Promotion.from_names(promote.requires)
|
|
87
|
-
result = policy.evaluate(
|
|
88
|
-
entry: env, schemas: @schemas, manifest: @manifest, role: @ctx.role,
|
|
89
|
-
)
|
|
90
|
-
return if result.ok?
|
|
91
|
-
|
|
92
|
-
raise ProposalError.new(
|
|
93
|
-
"promotion gate failed: #{result.reasons.join("; ")}",
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
Textus::Application::UseCase.register(:accept, Textus::Application::Write::Accept, caps: :write)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
# Shared gate for write verbs that require the caller to hold the
|
|
5
|
-
# manifest's accept_authority role. Provides one method, expressed
|
|
6
|
-
# as two early-returns rather than a ternary, so each failure mode
|
|
7
|
-
# reads on its own line.
|
|
8
|
-
module AuthorityGate
|
|
9
|
-
def assert_accept_authority!(verb)
|
|
10
|
-
return if @manifest.policy.role_kind(@ctx.role) == :accept_authority
|
|
11
|
-
|
|
12
|
-
authority = @manifest.policy.roles_with_kind(:accept_authority).first
|
|
13
|
-
if authority.nil?
|
|
14
|
-
raise ProposalError.new(
|
|
15
|
-
"no role with accept_authority kind is declared in this manifest; #{verb} is disabled",
|
|
16
|
-
)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
raise ProposalError.new(
|
|
20
|
-
"only #{authority} role can #{verb} proposals; got '#{@ctx.role}'",
|
|
21
|
-
)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Application
|
|
3
|
-
module Write
|
|
4
|
-
module Delete
|
|
5
|
-
def self.call(*, session:, ctx:, caps:, **)
|
|
6
|
-
Impl.new(
|
|
7
|
-
ctx: ctx, caps: caps,
|
|
8
|
-
writer: session.envelope_writer,
|
|
9
|
-
hook_context: session.hook_context
|
|
10
|
-
).call(*, **)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class Impl
|
|
14
|
-
def initialize(ctx:, caps:, writer:, hook_context:)
|
|
15
|
-
@ctx = ctx
|
|
16
|
-
@manifest = caps.manifest
|
|
17
|
-
@events = caps.events
|
|
18
|
-
@authorizer = caps.authorizer
|
|
19
|
-
@writer = writer
|
|
20
|
-
@hook_context = hook_context
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(key, if_etag: nil, suppress_events: false)
|
|
24
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
25
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
26
|
-
|
|
27
|
-
@authorizer.authorize_write!(mentry, role: @ctx.role)
|
|
28
|
-
|
|
29
|
-
@writer.delete(key, mentry: mentry, if_etag: if_etag)
|
|
30
|
-
|
|
31
|
-
unless suppress_events
|
|
32
|
-
@events.publish(:entry_deleted,
|
|
33
|
-
ctx: @hook_context,
|
|
34
|
-
key: key)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Textus::Application::UseCase.register(:delete, Textus::Application::Write::Delete, caps: :write)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
module Write
|
|
6
|
-
# Materializes a single Derived manifest entry onto disk by running
|
|
7
|
-
# the builder pipeline (template + projection + external runner).
|
|
8
|
-
# Extracted from Application::Write::Build so that Publish can reuse
|
|
9
|
-
# it without creating a Build dependency.
|
|
10
|
-
class Materializer
|
|
11
|
-
def initialize(ctx:, caps:, rpc:, session:)
|
|
12
|
-
@ctx = ctx
|
|
13
|
-
@caps = caps
|
|
14
|
-
@manifest = caps.manifest
|
|
15
|
-
@file_store = caps.file_store
|
|
16
|
-
@rpc = rpc
|
|
17
|
-
@root = caps.root
|
|
18
|
-
@session = session
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Runs the builder pipeline for `mentry` and returns the on-disk
|
|
22
|
-
# target_path string.
|
|
23
|
-
def run(mentry)
|
|
24
|
-
reader = Textus::Application::Read::Get::Impl.new(ctx: @ctx, caps: @caps)
|
|
25
|
-
lister = Textus::Application::Read::List::Impl.new(caps: @caps)
|
|
26
|
-
Builder::Pipeline.run(
|
|
27
|
-
mentry: mentry,
|
|
28
|
-
manifest: @manifest,
|
|
29
|
-
reader: reader.method(:call),
|
|
30
|
-
lister: lister.method(:call),
|
|
31
|
-
rpc: @rpc,
|
|
32
|
-
template_loader: ->(name) { read_template(name) },
|
|
33
|
-
transform_context: @caps,
|
|
34
|
-
inject_boot: -> { Textus::Boot.run(@session) },
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def read_template(name)
|
|
41
|
-
tpl_path = File.join(@root, "templates", name)
|
|
42
|
-
raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
|
|
43
|
-
|
|
44
|
-
File.read(tpl_path)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|