textus 0.30.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +2 -241
- data/CHANGELOG.md +113 -0
- data/README.md +83 -62
- data/SPEC.md +352 -211
- data/docs/conventions.md +42 -37
- data/lib/textus/boot.rb +89 -74
- data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
- data/lib/textus/cli/verb/build.rb +1 -1
- data/lib/textus/cli/verb/fetch.rb +14 -0
- data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/put.rb +1 -1
- data/lib/textus/cli/verb/rule_list.rb +7 -7
- data/lib/textus/cli.rb +2 -2
- data/lib/textus/container.rb +1 -2
- data/lib/textus/dispatcher.rb +3 -3
- data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
- data/lib/textus/doctor/check/proposal_targets.rb +45 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor.rb +2 -1
- data/lib/textus/domain/action.rb +3 -3
- data/lib/textus/domain/freshness/evaluator.rb +3 -3
- data/lib/textus/domain/freshness/policy.rb +2 -2
- data/lib/textus/domain/freshness.rb +7 -7
- data/lib/textus/domain/outcome.rb +2 -2
- data/lib/textus/domain/permission.rb +2 -10
- data/lib/textus/domain/policy/base_guards.rb +25 -0
- data/lib/textus/domain/policy/evaluation.rb +18 -0
- data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
- data/lib/textus/domain/policy/guard.rb +35 -0
- data/lib/textus/domain/policy/guard_factory.rb +40 -0
- data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
- data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
- data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
- data/lib/textus/domain/policy/predicates/registry.rb +39 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
- data/lib/textus/domain/staleness/intake_check.rb +6 -6
- data/lib/textus/envelope.rb +2 -2
- data/lib/textus/errors.rb +25 -28
- data/lib/textus/hooks/event_bus.rb +4 -4
- data/lib/textus/init.rb +23 -18
- data/lib/textus/maintenance/zone_mv.rb +1 -1
- data/lib/textus/manifest/capabilities.rb +29 -0
- data/lib/textus/manifest/data.rb +14 -10
- data/lib/textus/manifest/policy.rb +37 -21
- data/lib/textus/manifest/rules.rb +16 -14
- data/lib/textus/manifest/schema.rb +48 -58
- data/lib/textus/manifest.rb +3 -3
- data/lib/textus/mcp/server.rb +1 -1
- data/lib/textus/mcp/tool_schemas.rb +3 -3
- data/lib/textus/mcp/tools.rb +7 -7
- data/lib/textus/ports/audit_subscriber.rb +1 -1
- data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
- data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/read/freshness.rb +9 -9
- data/lib/textus/read/get.rb +8 -8
- data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
- data/lib/textus/read/policy_explain.rb +14 -10
- data/lib/textus/read/pulse.rb +5 -4
- data/lib/textus/read/validator.rb +1 -1
- data/lib/textus/schema/tools.rb +5 -5
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +19 -55
- data/lib/textus/write/delete.rb +14 -2
- data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
- data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
- data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +21 -14
- data/lib/textus/write/mv.rb +15 -3
- data/lib/textus/write/put.rb +14 -2
- data/lib/textus/write/reject.rb +11 -5
- metadata +24 -18
- data/lib/textus/cli/verb/refresh.rb +0 -14
- data/lib/textus/domain/authorizer.rb +0 -37
- data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
- data/lib/textus/domain/policy/promote.rb +0 -26
- data/lib/textus/domain/policy/promotion.rb +0 -57
- data/lib/textus/manifest/role_kinds.rb +0 -21
- data/lib/textus/write/authority_gate.rb +0 -24
|
@@ -29,16 +29,16 @@ module Textus
|
|
|
29
29
|
"meta" => { "type" => "object" },
|
|
30
30
|
"body" => { "type" => "string" },
|
|
31
31
|
}, %w[key meta]),
|
|
32
|
-
tool("
|
|
32
|
+
tool("fetch", "Run an intake fetch for one key. Returns the fetch Outcome.",
|
|
33
33
|
{ "key" => { "type" => "string" } }, ["key"]),
|
|
34
|
-
tool("
|
|
34
|
+
tool("fetch_stale", "Fetch all stale intake entries, optionally scoped by zone/prefix.",
|
|
35
35
|
{
|
|
36
36
|
"zone" => { "type" => "string" },
|
|
37
37
|
"prefix" => { "type" => "string" },
|
|
38
38
|
}, []),
|
|
39
39
|
tool("schema", "Return the schema (field shape) for an entry family.",
|
|
40
40
|
{ "family" => { "type" => "string" } }, ["family"]),
|
|
41
|
-
tool("rules", "Return effective rules for a key (
|
|
41
|
+
tool("rules", "Return effective rules for a key (fetch, promote, ...).",
|
|
42
42
|
{ "key" => { "type" => "string" } }, ["key"]),
|
|
43
43
|
tool("key_mv_prefix",
|
|
44
44
|
"Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false.",
|
data/lib/textus/mcp/tools.rb
CHANGED
|
@@ -64,14 +64,14 @@ module Textus
|
|
|
64
64
|
{ "uid" => env.uid, "etag" => env.etag, "key" => target }
|
|
65
65
|
end,
|
|
66
66
|
|
|
67
|
-
"
|
|
68
|
-
key = args.fetch("key") { raise ToolError.new("
|
|
69
|
-
outcome = ops_for(s, store).
|
|
67
|
+
"fetch" => lambda do |s, store, args|
|
|
68
|
+
key = args.fetch("key") { raise ToolError.new("fetch: missing key") }
|
|
69
|
+
outcome = ops_for(s, store).fetch(key)
|
|
70
70
|
{ "outcome" => outcome.class.name.split("::").last.downcase }
|
|
71
71
|
end,
|
|
72
72
|
|
|
73
|
-
"
|
|
74
|
-
ops_for(s, store).
|
|
73
|
+
"fetch_stale" => lambda do |s, store, args|
|
|
74
|
+
ops_for(s, store).fetch_all(zone: args["zone"], prefix: args["prefix"])
|
|
75
75
|
end,
|
|
76
76
|
|
|
77
77
|
"schema" => lambda do |_s, store, args|
|
|
@@ -83,8 +83,8 @@ module Textus
|
|
|
83
83
|
key = args.fetch("key") { raise ToolError.new("rules: missing key") }
|
|
84
84
|
set = store.manifest.rules.for(key)
|
|
85
85
|
{
|
|
86
|
-
"
|
|
87
|
-
"
|
|
86
|
+
"fetch" => set.fetch&.to_h,
|
|
87
|
+
"guard" => set.guard,
|
|
88
88
|
}.compact
|
|
89
89
|
end,
|
|
90
90
|
|
|
@@ -33,7 +33,7 @@ module Textus
|
|
|
33
33
|
extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
|
|
34
34
|
extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
|
|
35
35
|
@audit_log.append(
|
|
36
|
-
role: "
|
|
36
|
+
role: "automation", verb: "event_error", key: key,
|
|
37
37
|
etag_before: nil, etag_after: nil, extras: extras
|
|
38
38
|
)
|
|
39
39
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Ports
|
|
3
|
-
module
|
|
3
|
+
module Fetch
|
|
4
4
|
module Detached
|
|
5
5
|
module_function
|
|
6
6
|
|
|
@@ -16,14 +16,14 @@ module Textus
|
|
|
16
16
|
$stdout.reopen(File::NULL, "w")
|
|
17
17
|
$stderr.reopen(File::NULL, "w")
|
|
18
18
|
|
|
19
|
-
lock = Textus::Ports::
|
|
19
|
+
lock = Textus::Ports::Fetch::Lock.new(root: store_root, key: key)
|
|
20
20
|
exit(0) unless lock.try_acquire
|
|
21
21
|
|
|
22
22
|
begin
|
|
23
23
|
store = Textus::Store.new(store_root)
|
|
24
|
-
store.as("
|
|
24
|
+
store.as("automation").fetch(key)
|
|
25
25
|
rescue StandardError
|
|
26
|
-
# Already logged via :
|
|
26
|
+
# Already logged via :fetch_failed; exit cleanly.
|
|
27
27
|
ensure
|
|
28
28
|
lock.release
|
|
29
29
|
exit(0)
|
data/lib/textus/projection.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
|
|
9
9
|
# `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
|
|
10
10
|
# semantics: pure read (`ops.get`) for materialization paths;
|
|
11
|
-
# `ops.
|
|
11
|
+
# `ops.get_or_fetch` if you want fetch-on-stale.
|
|
12
12
|
# `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
|
|
13
13
|
# `rpc` — a `Hooks::RpcRegistry` used to dispatch `transform_rows` callables.
|
|
14
14
|
# `transform_context` — capability object handed to transform reducers as `caps:`.
|
|
@@ -3,8 +3,8 @@ require "time"
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Read
|
|
5
5
|
# Per-entry freshness report. Walks every entry declared in the manifest,
|
|
6
|
-
# consults `rules_for(key)` for a
|
|
7
|
-
# current status. Status is one of :fresh, :stale, :
|
|
6
|
+
# consults `rules_for(key)` for a fetch rule, and reports the
|
|
7
|
+
# current status. Status is one of :fresh, :stale, :never_fetched, or
|
|
8
8
|
# :no_policy.
|
|
9
9
|
class Freshness
|
|
10
10
|
def initialize(container:, call:, evaluator: Textus::Domain::Freshness::Evaluator)
|
|
@@ -16,7 +16,7 @@ module Textus
|
|
|
16
16
|
@cache = {}
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Returns the soonest `next_due_at` across all entries with a
|
|
19
|
+
# Returns the soonest `next_due_at` across all entries with a fetch
|
|
20
20
|
# policy, as an ISO-8601 string, or nil if none.
|
|
21
21
|
def soonest_due(prefix: nil, zone: nil)
|
|
22
22
|
times = call(prefix: prefix, zone: zone)
|
|
@@ -43,17 +43,17 @@ module Textus
|
|
|
43
43
|
|
|
44
44
|
def row_for(mentry)
|
|
45
45
|
set = @manifest.rules.for(mentry.key)
|
|
46
|
-
|
|
46
|
+
fetch = set.fetch
|
|
47
47
|
envelope = safe_get(mentry.key)
|
|
48
|
-
last = envelope&.meta&.dig("
|
|
48
|
+
last = envelope&.meta&.dig("last_fetched_at")
|
|
49
49
|
|
|
50
|
-
return base_row(mentry, last).merge(status: :no_policy) if
|
|
50
|
+
return base_row(mentry, last).merge(status: :no_policy) if fetch.nil?
|
|
51
51
|
|
|
52
|
-
fp =
|
|
52
|
+
fp = fetch.to_freshness_policy
|
|
53
53
|
cache_key = [mentry.key, last]
|
|
54
54
|
verdict = (@cache[cache_key] ||= @evaluator.call(fp, envelope, now: @call.now))
|
|
55
55
|
status = if verdict.fresh? then :fresh
|
|
56
|
-
elsif last.nil? then :
|
|
56
|
+
elsif last.nil? then :never_fetched
|
|
57
57
|
else :stale
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -69,7 +69,7 @@ module Textus
|
|
|
69
69
|
{
|
|
70
70
|
key: mentry.key,
|
|
71
71
|
zone: mentry.zone,
|
|
72
|
-
|
|
72
|
+
last_fetched_at: last,
|
|
73
73
|
age_seconds: last ? (@call.now - Time.parse(last)).to_i : nil,
|
|
74
74
|
}
|
|
75
75
|
end
|
data/lib/textus/read/get.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
3
|
# Pure read: returns the on-disk envelope annotated with a freshness
|
|
4
|
-
# verdict. Never triggers
|
|
4
|
+
# verdict. Never triggers fetch; never invokes the orchestrator.
|
|
5
5
|
#
|
|
6
|
-
# For interactive reads that want
|
|
7
|
-
# `Read::
|
|
6
|
+
# For interactive reads that want fetch-on-stale, use
|
|
7
|
+
# `Read::GetOrFetch`, which composes this with the orchestrator.
|
|
8
8
|
class Get
|
|
9
9
|
def initialize(container:, call:, evaluator: Textus::Domain::Freshness::Evaluator)
|
|
10
10
|
@container = container
|
|
@@ -19,16 +19,16 @@ module Textus
|
|
|
19
19
|
return nil if envelope.nil?
|
|
20
20
|
|
|
21
21
|
policy_set = @manifest.rules.for(key)
|
|
22
|
-
|
|
23
|
-
return annotate_fresh(envelope) if
|
|
22
|
+
fetch_policy = policy_set.fetch
|
|
23
|
+
return annotate_fresh(envelope) if fetch_policy.nil?
|
|
24
24
|
|
|
25
|
-
policy =
|
|
25
|
+
policy = fetch_policy.to_freshness_policy
|
|
26
26
|
verdict = @evaluator.call(policy, envelope, now: @call.now)
|
|
27
27
|
|
|
28
28
|
envelope.with(freshness: Textus::Domain::Freshness.build(
|
|
29
29
|
stale: verdict.stale?,
|
|
30
30
|
reason: verdict.reason,
|
|
31
|
-
|
|
31
|
+
fetching: false,
|
|
32
32
|
))
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -58,7 +58,7 @@ module Textus
|
|
|
58
58
|
|
|
59
59
|
def annotate_fresh(envelope)
|
|
60
60
|
envelope.with(freshness: Textus::Domain::Freshness.build(
|
|
61
|
-
stale: false, reason: nil,
|
|
61
|
+
stale: false, reason: nil, fetching: false,
|
|
62
62
|
))
|
|
63
63
|
end
|
|
64
64
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
|
-
# Composes pure `Read::Get` with the
|
|
3
|
+
# Composes pure `Read::Get` with the fetch orchestrator: runs Get
|
|
4
4
|
# to obtain the envelope and freshness verdict, then if the verdict
|
|
5
5
|
# is stale and the rule's `on_stale` policy demands action, hands
|
|
6
6
|
# off to the orchestrator. Use for interactive reads where the
|
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
#
|
|
9
9
|
# Pure reads (build, projection, schema tooling) should use
|
|
10
10
|
# `Read::Get` directly; it has no orchestrator dependency.
|
|
11
|
-
class
|
|
11
|
+
class GetOrFetch
|
|
12
12
|
def initialize(container:, call:, get: nil, orchestrator: nil)
|
|
13
13
|
@container = container
|
|
14
14
|
@call = call
|
|
@@ -24,10 +24,10 @@ module Textus
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def build_orchestrator
|
|
27
|
-
worker = Textus::Write::
|
|
27
|
+
worker = Textus::Write::FetchWorker.new(
|
|
28
28
|
container: @container, call: @call,
|
|
29
29
|
)
|
|
30
|
-
Textus::Write::
|
|
30
|
+
Textus::Write::FetchOrchestrator.new(
|
|
31
31
|
worker: worker, store_root: @container.root, events: @container.events,
|
|
32
32
|
hook_context: hook_context
|
|
33
33
|
)
|
|
@@ -41,10 +41,10 @@ module Textus
|
|
|
41
41
|
return envelope unless envelope.freshness&.stale
|
|
42
42
|
|
|
43
43
|
policy_set = @manifest.rules.for(key)
|
|
44
|
-
|
|
45
|
-
return envelope if
|
|
44
|
+
fetch_policy = policy_set.fetch
|
|
45
|
+
return envelope if fetch_policy.nil?
|
|
46
46
|
|
|
47
|
-
policy =
|
|
47
|
+
policy = fetch_policy.to_freshness_policy
|
|
48
48
|
verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
|
|
49
49
|
action = policy.decide(verdict)
|
|
50
50
|
outcome = @orchestrator.execute(action, key: key)
|
|
@@ -52,15 +52,15 @@ module Textus
|
|
|
52
52
|
case outcome
|
|
53
53
|
when Textus::Domain::Outcome::Skipped
|
|
54
54
|
envelope
|
|
55
|
-
when Textus::Domain::Outcome::
|
|
55
|
+
when Textus::Domain::Outcome::Fetched
|
|
56
56
|
outcome.envelope.with(
|
|
57
|
-
freshness: Textus::Domain::Freshness.build(stale: false, reason: nil,
|
|
57
|
+
freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, fetching: false),
|
|
58
58
|
)
|
|
59
59
|
when Textus::Domain::Outcome::Detached
|
|
60
|
-
envelope.with(freshness: envelope.freshness.with(
|
|
60
|
+
envelope.with(freshness: envelope.freshness.with(fetching: true))
|
|
61
61
|
when Textus::Domain::Outcome::Failed
|
|
62
62
|
envelope.with(
|
|
63
|
-
freshness: envelope.freshness.with(
|
|
63
|
+
freshness: envelope.freshness.with(fetch_error: outcome.error.message),
|
|
64
64
|
)
|
|
65
65
|
end
|
|
66
66
|
end
|
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
3
|
# For one key, surface every matching policy block along with the
|
|
4
|
-
# per-slot effective value (which loses ties win-by-specificity)
|
|
4
|
+
# per-slot effective value (which loses ties win-by-specificity) and the
|
|
5
|
+
# effective guard predicate names for every write transition (ADR 0031).
|
|
5
6
|
class PolicyExplain
|
|
6
7
|
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
8
|
@manifest = container.manifest
|
|
9
|
+
@schemas = container.schemas
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def call(key:)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
matching = @manifest.rules.explain(key)
|
|
14
|
+
winners = @manifest.rules.for(key)
|
|
15
|
+
factory = Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
14
16
|
|
|
15
17
|
{
|
|
16
18
|
key: key,
|
|
17
19
|
matched_blocks: matching.map do |b|
|
|
18
20
|
{
|
|
19
21
|
match: b.match,
|
|
20
|
-
|
|
22
|
+
fetch: !b.fetch.nil?,
|
|
21
23
|
handler_allowlist: !b.handler_allowlist.nil?,
|
|
22
|
-
|
|
24
|
+
guard: !b.guard.nil?,
|
|
23
25
|
retention: !b.retention.nil?,
|
|
24
26
|
}
|
|
25
27
|
end,
|
|
26
28
|
effective: {
|
|
27
|
-
|
|
28
|
-
ttl_seconds: winners.
|
|
29
|
-
on_stale: winners.
|
|
29
|
+
fetch: winners.fetch && {
|
|
30
|
+
ttl_seconds: winners.fetch.ttl_seconds,
|
|
31
|
+
on_stale: winners.fetch.on_stale,
|
|
30
32
|
},
|
|
31
33
|
handler_allowlist: winners.handler_allowlist&.handlers,
|
|
32
|
-
promotion: winners.promote && { requires: winners.promote.requires },
|
|
33
34
|
retention: winners.retention && {
|
|
34
35
|
expire_after: winners.retention.expire_after,
|
|
35
36
|
archive_after: winners.retention.archive_after,
|
|
36
37
|
},
|
|
37
38
|
},
|
|
39
|
+
guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
|
|
40
|
+
[transition, factory.for(transition, key).predicates.map(&:name)]
|
|
41
|
+
end,
|
|
38
42
|
}
|
|
39
43
|
end
|
|
40
44
|
end
|
data/lib/textus/read/pulse.rb
CHANGED
|
@@ -49,11 +49,12 @@ module Textus
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def review_keys
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
52
|
+
# The single queue zone (kind: queue; schema guarantees ≤1), derived
|
|
53
|
+
# from the manifest rather than a hardcoded zone name (ADR 0034 / D1).
|
|
54
|
+
queue = @manifest.policy.queue_zone
|
|
55
|
+
return [] unless queue
|
|
55
56
|
|
|
56
|
-
rows = Read::List.new(container: @container).call(zone:
|
|
57
|
+
rows = Read::List.new(container: @container).call(zone: queue)
|
|
57
58
|
rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
|
|
58
59
|
end
|
|
59
60
|
|
|
@@ -54,7 +54,7 @@ module Textus
|
|
|
54
54
|
last_writer = @audit_log.last_writer_for(key)
|
|
55
55
|
return if last_writer.nil?
|
|
56
56
|
|
|
57
|
-
last_writer_is_authority = @manifest.policy.
|
|
57
|
+
last_writer_is_authority = @manifest.policy.roles_with_capability("author").include?(last_writer)
|
|
58
58
|
|
|
59
59
|
env.meta.each_key do |field|
|
|
60
60
|
owner = schema.maintained_by(field)
|
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Textus
|
|
|
49
49
|
end
|
|
50
50
|
raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
|
|
51
51
|
|
|
52
|
-
authority =
|
|
52
|
+
authority = accept_role_for(store)
|
|
53
53
|
ops = store.as(authority)
|
|
54
54
|
touched = []
|
|
55
55
|
store.manifest.resolver.enumerate.each do |row|
|
|
@@ -87,13 +87,13 @@ module Textus
|
|
|
87
87
|
raise UsageError.new("schema not found: #{name}")
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
def self.
|
|
91
|
-
authority = store.manifest.policy.
|
|
90
|
+
def self.accept_role_for(store)
|
|
91
|
+
authority = store.manifest.policy.roles_with_capability("author").first
|
|
92
92
|
return authority if authority
|
|
93
93
|
|
|
94
94
|
raise UsageError.new(
|
|
95
|
-
"schema migrate requires a role
|
|
96
|
-
"none declared (add e.g. `- { name: owner,
|
|
95
|
+
"schema migrate requires a role holding the 'author' capability; " \
|
|
96
|
+
"none declared (add e.g. `- { name: owner, can: [author] }` to roles:)",
|
|
97
97
|
)
|
|
98
98
|
end
|
|
99
99
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus/write/accept.rb
CHANGED
|
@@ -1,37 +1,30 @@
|
|
|
1
|
-
require_relative "authority_gate"
|
|
2
|
-
|
|
3
1
|
module Textus
|
|
4
2
|
module Write
|
|
5
3
|
class Accept
|
|
6
|
-
include AuthorityGate
|
|
7
|
-
|
|
8
4
|
def initialize(container:, call:)
|
|
9
|
-
@container
|
|
10
|
-
@call
|
|
11
|
-
@manifest
|
|
12
|
-
@schemas
|
|
13
|
-
@events
|
|
5
|
+
@container = container
|
|
6
|
+
@call = call
|
|
7
|
+
@manifest = container.manifest
|
|
8
|
+
@schemas = container.schemas
|
|
9
|
+
@events = container.events
|
|
14
10
|
end
|
|
15
11
|
|
|
16
12
|
def call(pending_key)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
env = Textus::Read::Get.new(
|
|
20
|
-
container: @container, call: @call,
|
|
21
|
-
).call(pending_key)
|
|
13
|
+
env = Textus::Read::Get.new(container: @container, call: @call).call(pending_key)
|
|
22
14
|
proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
23
15
|
target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
24
16
|
action = proposal["action"] || "put"
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
guard.for(:accept, target).check!(
|
|
19
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
20
|
+
actor: @call.role, transition: :accept, origin: pending_key,
|
|
21
|
+
target: target, envelope: env, snapshot: @manifest
|
|
22
|
+
),
|
|
23
|
+
)
|
|
27
24
|
|
|
28
25
|
case action
|
|
29
26
|
when "put"
|
|
30
|
-
|
|
31
|
-
# target. Not related to the removed intake-handler legacy bridge.
|
|
32
|
-
target_meta = env.meta["frontmatter"] || {}
|
|
33
|
-
target_body = env.body
|
|
34
|
-
put_op.call(target, meta: target_meta, body: target_body)
|
|
27
|
+
put_op.call(target, meta: env.meta["frontmatter"] || {}, body: env.body)
|
|
35
28
|
when "delete"
|
|
36
29
|
delete_op.call(target)
|
|
37
30
|
else
|
|
@@ -39,48 +32,19 @@ module Textus
|
|
|
39
32
|
end
|
|
40
33
|
|
|
41
34
|
delete_op.call(pending_key)
|
|
42
|
-
|
|
43
|
-
@events.publish(:proposal_accepted,
|
|
44
|
-
ctx: hook_context,
|
|
45
|
-
key: pending_key,
|
|
46
|
-
target_key: target)
|
|
47
|
-
|
|
35
|
+
@events.publish(:proposal_accepted, ctx: hook_context, key: pending_key, target_key: target)
|
|
48
36
|
{ "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
|
|
49
37
|
end
|
|
50
38
|
|
|
51
39
|
private
|
|
52
40
|
|
|
53
|
-
def
|
|
54
|
-
@
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def put_op
|
|
58
|
-
@put_op ||= Textus::Write::Put.new(
|
|
59
|
-
container: @container, call: @call,
|
|
60
|
-
)
|
|
41
|
+
def guard
|
|
42
|
+
@guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
61
43
|
end
|
|
62
44
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def evaluate_promotion!(env, target_key)
|
|
70
|
-
rules = @manifest.rules.for(target_key)
|
|
71
|
-
promote = rules.promote
|
|
72
|
-
return if promote.nil? || promote.requires.empty?
|
|
73
|
-
|
|
74
|
-
policy = Textus::Domain::Policy::Promotion.from_names(promote.requires)
|
|
75
|
-
result = policy.evaluate(
|
|
76
|
-
entry: env, schemas: @schemas, manifest: @manifest, role: @call.role,
|
|
77
|
-
)
|
|
78
|
-
return if result.ok?
|
|
79
|
-
|
|
80
|
-
raise ProposalError.new(
|
|
81
|
-
"promotion gate failed: #{result.reasons.join("; ")}",
|
|
82
|
-
)
|
|
83
|
-
end
|
|
45
|
+
def hook_context = @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
46
|
+
def put_op = @put_op ||= Textus::Write::Put.new(container: @container, call: @call)
|
|
47
|
+
def delete_op = @delete_op ||= Textus::Write::Delete.new(container: @container, call: @call)
|
|
84
48
|
end
|
|
85
49
|
end
|
|
86
50
|
end
|
data/lib/textus/write/delete.rb
CHANGED
|
@@ -5,7 +5,6 @@ module Textus
|
|
|
5
5
|
@container = container
|
|
6
6
|
@call = call
|
|
7
7
|
@manifest = container.manifest
|
|
8
|
-
@authorizer = container.authorizer
|
|
9
8
|
@events = container.events
|
|
10
9
|
end
|
|
11
10
|
|
|
@@ -13,7 +12,7 @@ module Textus
|
|
|
13
12
|
Textus::Manifest::Data.validate_key!(key)
|
|
14
13
|
mentry = @manifest.resolver.resolve(key).entry
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
guard_for(:delete, key, if_etag: if_etag).check!(eval_for(:delete, target_key: key))
|
|
17
16
|
|
|
18
17
|
writer.delete(key, mentry: mentry, if_etag: if_etag)
|
|
19
18
|
|
|
@@ -28,6 +27,19 @@ module Textus
|
|
|
28
27
|
|
|
29
28
|
private
|
|
30
29
|
|
|
30
|
+
def guard_for(transition, key, if_etag: nil)
|
|
31
|
+
Textus::Domain::Policy::GuardFactory.new(
|
|
32
|
+
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
33
|
+
).for(transition, key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def eval_for(transition, target_key:, envelope: nil)
|
|
37
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
38
|
+
actor: @call.role, transition: transition, origin: nil,
|
|
39
|
+
target: target_key, envelope: envelope, snapshot: @manifest
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
31
43
|
def hook_context
|
|
32
44
|
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
33
45
|
end
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
|
-
class
|
|
3
|
+
class FetchAll
|
|
4
4
|
def initialize(container:, call:)
|
|
5
5
|
@container = container
|
|
6
6
|
@call = call
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call(prefix: nil, zone: nil)
|
|
10
|
-
worker = Textus::Write::
|
|
10
|
+
worker = Textus::Write::FetchWorker.new(
|
|
11
11
|
container: @container, call: @call,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
stale_rows = Textus::Read::Stale.new(container: @container, call: @call).call(prefix: prefix, zone: zone)
|
|
15
|
-
|
|
15
|
+
fetched = []
|
|
16
16
|
failed = []
|
|
17
17
|
skipped = []
|
|
18
18
|
|
|
19
19
|
stale_rows.each do |row|
|
|
20
20
|
key = row["key"] || row[:key]
|
|
21
21
|
reason = row["reason"] || row[:reason]
|
|
22
|
-
if reason.to_s.match?(/ttl exceeded|never
|
|
22
|
+
if reason.to_s.match?(/ttl exceeded|never fetched/)
|
|
23
23
|
begin
|
|
24
24
|
worker.run(key)
|
|
25
|
-
|
|
25
|
+
fetched << key
|
|
26
26
|
rescue Textus::Error => e
|
|
27
27
|
failed << { "key" => key, "error" => e.message }
|
|
28
28
|
end
|
|
@@ -34,7 +34,7 @@ module Textus
|
|
|
34
34
|
{
|
|
35
35
|
"protocol" => Textus::PROTOCOL,
|
|
36
36
|
"ok" => failed.empty?,
|
|
37
|
-
"
|
|
37
|
+
"fetched" => fetched,
|
|
38
38
|
"failed" => failed,
|
|
39
39
|
"skipped" => skipped,
|
|
40
40
|
}
|