textus 0.47.1 → 0.49.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/CHANGELOG.md +32 -0
- data/README.md +9 -7
- data/SPEC.md +40 -46
- data/docs/reference/conventions.md +11 -10
- data/lib/textus/boot.rb +2 -2
- data/lib/textus/cli/runner.rb +5 -4
- data/lib/textus/dispatcher.rb +3 -8
- data/lib/textus/doctor/check/generator_drift.rb +28 -0
- data/lib/textus/doctor/check/lifecycle_action_invalid.rb +39 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +1 -1
- data/lib/textus/doctor.rb +2 -0
- data/lib/textus/domain/lifecycle.rb +83 -0
- data/lib/textus/domain/policy/base_guards.rb +2 -2
- data/lib/textus/domain/policy/lifecycle.rb +35 -0
- data/lib/textus/domain/staleness.rb +6 -3
- data/lib/textus/envelope/io/writer.rb +2 -2
- data/lib/textus/hooks/context.rb +1 -1
- data/lib/textus/init.rb +4 -4
- data/lib/textus/maintenance/key_delete_prefix.rb +1 -1
- data/lib/textus/maintenance/key_mv_prefix.rb +2 -2
- data/lib/textus/maintenance/tend.rb +110 -0
- data/lib/textus/manifest/rules.rb +11 -23
- data/lib/textus/manifest/schema.rb +3 -18
- data/lib/textus/ports/audit_log.rb +1 -1
- data/lib/textus/ports/audit_subscriber.rb +1 -1
- data/lib/textus/ports/fetch/detached.rb +5 -1
- data/lib/textus/read/freshness.rb +29 -22
- data/lib/textus/read/get.rb +47 -32
- data/lib/textus/read/pulse.rb +1 -1
- data/lib/textus/read/rule_explain.rb +10 -16
- data/lib/textus/read/rule_list.rb +5 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +1 -1
- data/lib/textus/write/fetch_worker.rb +8 -12
- data/lib/textus/write/{delete.rb → key_delete.rb} +3 -3
- data/lib/textus/write/{mv.rb → key_mv.rb} +4 -4
- data/lib/textus/write/reject.rb +1 -1
- metadata +8 -15
- data/lib/textus/cli/group/fetch.rb +0 -20
- data/lib/textus/cli/verb/fetch.rb +0 -14
- data/lib/textus/cli/verb/fetch_all.rb +0 -20
- data/lib/textus/domain/policy/fetch.rb +0 -37
- data/lib/textus/domain/policy/retention.rb +0 -26
- data/lib/textus/domain/retention.rb +0 -44
- data/lib/textus/domain/staleness/intake_check.rb +0 -54
- data/lib/textus/maintenance/migrate.rb +0 -65
- data/lib/textus/read/retainable.rb +0 -17
- data/lib/textus/read/stale.rb +0 -17
- data/lib/textus/write/fetch_all.rb +0 -53
- data/lib/textus/write/retention_sweep.rb +0 -64
data/lib/textus/read/get.rb
CHANGED
|
@@ -2,42 +2,39 @@ module Textus
|
|
|
2
2
|
module Read
|
|
3
3
|
# The one read path. `fetch:` controls behavior:
|
|
4
4
|
# fetch: false (default) — pure read: the on-disk envelope annotated with
|
|
5
|
-
# a freshness verdict. NEVER builds the orchestrator
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# result when the key has no rule).
|
|
5
|
+
# a lifecycle freshness verdict. NEVER builds the orchestrator and NEVER
|
|
6
|
+
# mutates. Safe for direct callers (accept/reject/publish, materializer,
|
|
7
|
+
# uid, validators, hooks).
|
|
8
|
+
# fetch: true — read-through: after a stale verdict on a `refresh` policy,
|
|
9
|
+
# hands off to the fetch orchestrator. A read NEVER performs a
|
|
10
|
+
# destructive action (drop/archive) — those belong to the `tend` sweep
|
|
11
|
+
# (ADR 0079).
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
# `arg :fetch, default: true`, injected on every verb surface (RoleScope +
|
|
16
|
-
# MCP map_args, ADR 0062 amendment). Direct construction bypasses that
|
|
17
|
-
# injection and so gets the safe `fetch: false` method default.
|
|
13
|
+
# Lifecycle policy comes from the unified `lifecycle:` rule slot (ADR 0079).
|
|
18
14
|
class Get
|
|
19
15
|
extend Textus::Contract::DSL
|
|
20
16
|
|
|
21
17
|
verb :get
|
|
22
|
-
summary "Read one entry. Read-through by default —
|
|
23
|
-
"the entry's
|
|
24
|
-
"has no rule. Pass fetch:false for a
|
|
25
|
-
"read. Returns the envelope (uid, etag,
|
|
18
|
+
summary "Read one entry. Read-through by default — refreshes on stale per " \
|
|
19
|
+
"the entry's lifecycle rule (on_expire: refresh), degrading to a " \
|
|
20
|
+
"pure read when the key has no rule. Pass fetch:false for a " \
|
|
21
|
+
"guaranteed pure on-disk read. Returns the envelope (uid, etag, " \
|
|
22
|
+
"_meta, body, freshness)."
|
|
26
23
|
surfaces :cli, :mcp
|
|
27
24
|
arg :key, String, required: true, positional: true,
|
|
28
25
|
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
29
26
|
arg :fetch, :boolean, default: true,
|
|
30
|
-
description: "read-through (
|
|
31
|
-
"entry's
|
|
27
|
+
description: "read-through (refresh on stale per the " \
|
|
28
|
+
"entry's lifecycle rule) when true, the default; " \
|
|
32
29
|
"false returns the on-disk envelope without ever fetching"
|
|
33
30
|
view { |v, _i| v.to_h_for_wire }
|
|
34
31
|
|
|
35
|
-
def initialize(container:, call:,
|
|
32
|
+
def initialize(container:, call:, orchestrator: nil, file_stat: Textus::Ports::Storage::FileStat.new)
|
|
36
33
|
@container = container
|
|
37
34
|
@call = call
|
|
38
35
|
@manifest = container.manifest
|
|
39
36
|
@file_store = container.file_store
|
|
40
|
-
@
|
|
37
|
+
@file_stat = file_stat
|
|
41
38
|
@orchestrator = orchestrator # nil → built lazily on first fetch only
|
|
42
39
|
end
|
|
43
40
|
|
|
@@ -46,12 +43,11 @@ module Textus
|
|
|
46
43
|
return envelope if envelope.nil?
|
|
47
44
|
return envelope unless fetch && envelope.freshness&.stale
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
return envelope
|
|
46
|
+
policy = lifecycle_for(key)
|
|
47
|
+
return envelope unless policy&.on_expire == :refresh # only refresh acts on a read
|
|
51
48
|
|
|
52
|
-
policy = fetch_policy.to_freshness_policy
|
|
53
49
|
verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
|
|
54
|
-
outcome = orchestrator.execute(policy.decide(verdict), key: key)
|
|
50
|
+
outcome = orchestrator.execute(refresh_policy(policy).decide(verdict), key: key)
|
|
55
51
|
resolve(outcome, envelope)
|
|
56
52
|
end
|
|
57
53
|
|
|
@@ -64,23 +60,42 @@ module Textus
|
|
|
64
60
|
|
|
65
61
|
private
|
|
66
62
|
|
|
67
|
-
# Pure read +
|
|
63
|
+
# Pure read + unified lifecycle verdict; no orchestrator dependency.
|
|
68
64
|
def annotated_envelope(key)
|
|
69
65
|
envelope = read_raw_envelope(key)
|
|
70
66
|
return nil if envelope.nil?
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
return annotate_fresh(envelope) if
|
|
68
|
+
policy = lifecycle_for(key)
|
|
69
|
+
return annotate_fresh(envelope) if policy.nil?
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
expired, reason = Textus::Domain::Lifecycle.verdict(
|
|
72
|
+
policy: policy,
|
|
73
|
+
last_fetched_at: envelope.meta&.dig("last_fetched_at"),
|
|
74
|
+
mtime: mtime_for(key),
|
|
75
|
+
now: @call.now,
|
|
76
|
+
)
|
|
77
77
|
envelope.with(freshness: Textus::Domain::Freshness.build(
|
|
78
|
-
stale:
|
|
78
|
+
stale: expired, reason: reason, fetching: false,
|
|
79
79
|
))
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
def
|
|
83
|
-
@manifest.rules.for(key).
|
|
82
|
+
def lifecycle_for(key)
|
|
83
|
+
@manifest.rules.for(key).lifecycle
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def refresh_policy(policy)
|
|
87
|
+
Textus::Domain::Freshness::Policy.new(
|
|
88
|
+
ttl_seconds: policy.ttl_seconds,
|
|
89
|
+
on_stale: policy.budget_ms ? :timed_sync : :sync,
|
|
90
|
+
sync_budget_ms: policy.budget_ms,
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def mtime_for(key)
|
|
95
|
+
path = @manifest.resolver.resolve(key).path
|
|
96
|
+
@file_stat.exists?(path) ? @file_stat.mtime(path) : nil
|
|
97
|
+
rescue Textus::Error
|
|
98
|
+
nil
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
def resolve(outcome, envelope)
|
data/lib/textus/read/pulse.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Textus
|
|
|
30
30
|
{
|
|
31
31
|
"cursor" => @audit_log.latest_seq,
|
|
32
32
|
"changed" => audit_changes_since(since),
|
|
33
|
-
"stale" => freshness_rows.select { |r| r[:status] == :
|
|
33
|
+
"stale" => freshness_rows.select { |r| r[:status] == :expired }.map { |r| r[:key] },
|
|
34
34
|
"pending_review" => review_keys,
|
|
35
35
|
"doctor" => doctor_summary,
|
|
36
36
|
"contract_etag" => contract_etag,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Read
|
|
3
3
|
# Effective rules for a key, at two depths (ADR 0059). Lean by default —
|
|
4
|
-
# `{
|
|
4
|
+
# `{ lifecycle, guard }`, the agent-cheap read that was the `rules` verb. With
|
|
5
5
|
# `detail: true` it returns the verbose explanation — every matching policy
|
|
6
6
|
# block plus the per-transition guard predicate names — that was
|
|
7
7
|
# `policy_explain`. One verb, one name across CLI/MCP/method; the audience
|
|
@@ -10,7 +10,7 @@ module Textus
|
|
|
10
10
|
extend Textus::Contract::DSL
|
|
11
11
|
|
|
12
12
|
verb :rule_explain
|
|
13
|
-
summary "Effective rules for a key. Lean {
|
|
13
|
+
summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
|
|
14
14
|
surfaces :cli, :mcp
|
|
15
15
|
cli "rule explain"
|
|
16
16
|
arg :key, String, required: true, positional: true,
|
|
@@ -34,11 +34,10 @@ module Textus
|
|
|
34
34
|
def effective(key)
|
|
35
35
|
set = @manifest.rules.for(key)
|
|
36
36
|
{
|
|
37
|
-
"
|
|
38
|
-
"ttl_seconds" => set.
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"fetch_timeout_seconds" => set.fetch.fetch_timeout_seconds,
|
|
37
|
+
"lifecycle" => set.lifecycle && {
|
|
38
|
+
"ttl_seconds" => set.lifecycle.ttl_seconds,
|
|
39
|
+
"on_expire" => set.lifecycle.on_expire,
|
|
40
|
+
"budget_ms" => set.lifecycle.budget_ms,
|
|
42
41
|
},
|
|
43
42
|
"guard" => set.guard,
|
|
44
43
|
}.compact
|
|
@@ -57,22 +56,17 @@ module Textus
|
|
|
57
56
|
matched_blocks: matching.map do |b|
|
|
58
57
|
{
|
|
59
58
|
match: b.match,
|
|
60
|
-
|
|
59
|
+
lifecycle: !b.lifecycle.nil?,
|
|
61
60
|
handler_allowlist: !b.handler_allowlist.nil?,
|
|
62
61
|
guard: !b.guard.nil?,
|
|
63
|
-
retention: !b.retention.nil?,
|
|
64
62
|
}
|
|
65
63
|
end,
|
|
66
64
|
effective: {
|
|
67
|
-
|
|
68
|
-
ttl_seconds: winners.
|
|
69
|
-
|
|
65
|
+
lifecycle: winners.lifecycle && {
|
|
66
|
+
ttl_seconds: winners.lifecycle.ttl_seconds,
|
|
67
|
+
on_expire: winners.lifecycle.on_expire,
|
|
70
68
|
},
|
|
71
69
|
handler_allowlist: winners.handler_allowlist&.handlers,
|
|
72
|
-
retention: winners.retention && {
|
|
73
|
-
expire_after: winners.retention.expire_after,
|
|
74
|
-
archive_after: winners.retention.archive_after,
|
|
75
|
-
},
|
|
76
70
|
},
|
|
77
71
|
guards: Textus::Domain::Policy::BaseGuards::BASE.keys.to_h do |transition|
|
|
78
72
|
[transition, factory.for(transition, key).predicates.map(&:name)]
|
|
@@ -20,17 +20,15 @@ module Textus
|
|
|
20
20
|
def call
|
|
21
21
|
@manifest.rules.blocks.map do |b|
|
|
22
22
|
row = { "match" => b.match }
|
|
23
|
-
if b.
|
|
24
|
-
row["
|
|
25
|
-
"ttl_seconds" => b.
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"fetch_timeout_seconds" => b.fetch.fetch_timeout_seconds,
|
|
23
|
+
if b.lifecycle
|
|
24
|
+
row["lifecycle"] = {
|
|
25
|
+
"ttl_seconds" => b.lifecycle.ttl_seconds,
|
|
26
|
+
"on_expire" => b.lifecycle.on_expire,
|
|
27
|
+
"budget_ms" => b.lifecycle.budget_ms,
|
|
29
28
|
}
|
|
30
29
|
end
|
|
31
30
|
row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
|
|
32
31
|
row["guard"] = b.guard if b.guard
|
|
33
|
-
row["retention"] = { "expire_after" => b.retention.expire_after, "archive_after" => b.retention.archive_after } if b.retention
|
|
34
32
|
row
|
|
35
33
|
end
|
|
36
34
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus/write/accept.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Textus
|
|
|
52
52
|
|
|
53
53
|
def hook_context = @hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
54
54
|
def put_op = @put_op ||= Textus::Write::Put.new(container: @container, call: @call)
|
|
55
|
-
def delete_op = @delete_op ||= Textus::Write::
|
|
55
|
+
def delete_op = @delete_op ||= Textus::Write::KeyDelete.new(container: @container, call: @call)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -2,16 +2,10 @@ require "timeout"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Write
|
|
5
|
+
# Internal fetch executor for one quarantine/intake entry. No longer a
|
|
6
|
+
# public verb (ADR 0079 collapsed the `fetch` surface): used by `get`'s
|
|
7
|
+
# orchestrator (read-through refresh) and by the `tend` sweep.
|
|
5
8
|
class FetchWorker
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :fetch
|
|
9
|
-
summary "Run a fetch action for one quarantine entry."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
arg :key, String, required: true, positional: true,
|
|
12
|
-
description: "quarantine-zone entry key to refresh using its declared intake action"
|
|
13
|
-
view { |outcome| { "outcome" => outcome.class.name.split("::").last.downcase } }
|
|
14
|
-
|
|
15
9
|
FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
|
|
16
10
|
|
|
17
11
|
def initialize(container:, call:)
|
|
@@ -69,9 +63,11 @@ module Textus
|
|
|
69
63
|
@fetch_events ||= FetchEvents.from(container: @container, call: @call)
|
|
70
64
|
end
|
|
71
65
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
# ADR 0079: a per-rule fetch_timeout_seconds override was an accepted loss
|
|
67
|
+
# in the fetch:/retention: → lifecycle: collapse; the constant ceiling
|
|
68
|
+
# applies to every intake.
|
|
69
|
+
def fetch_timeout_for(_key)
|
|
70
|
+
FETCH_TIMEOUT_SECONDS
|
|
75
71
|
end
|
|
76
72
|
|
|
77
73
|
def fetch_with_events(key, mentry, remaining)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
|
-
class
|
|
3
|
+
class KeyDelete
|
|
4
4
|
extend Textus::Contract::DSL
|
|
5
5
|
|
|
6
|
-
verb :
|
|
6
|
+
verb :key_delete
|
|
7
7
|
summary "Delete one entry by key. Single-key, lower blast radius than " \
|
|
8
8
|
"key_delete_prefix; guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
|
|
9
9
|
surfaces :cli, :mcp
|
|
@@ -25,7 +25,7 @@ module Textus
|
|
|
25
25
|
Textus::Manifest::Data.validate_key!(key)
|
|
26
26
|
mentry = @manifest.resolver.resolve(key).entry
|
|
27
27
|
|
|
28
|
-
guard_for(:
|
|
28
|
+
guard_for(:key_delete, key, if_etag: if_etag).check!(eval_for(:key_delete, target_key: key))
|
|
29
29
|
|
|
30
30
|
writer.delete(key, mentry: mentry, if_etag: if_etag)
|
|
31
31
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
|
-
class
|
|
3
|
+
class KeyMv
|
|
4
4
|
extend Textus::Contract::DSL
|
|
5
5
|
|
|
6
|
-
verb :
|
|
6
|
+
verb :key_mv
|
|
7
7
|
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
8
8
|
surfaces :cli, :mcp
|
|
9
9
|
cli "key mv"
|
|
@@ -53,8 +53,8 @@ module Textus
|
|
|
53
53
|
raise UnknownKey.new(old_key) unless reader.exists?(old_key)
|
|
54
54
|
|
|
55
55
|
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
56
|
-
guard_for(:
|
|
57
|
-
guard_for(:
|
|
56
|
+
guard_for(:key_mv, old_key).check!(eval_for(:key_mv, target_key: old_key))
|
|
57
|
+
guard_for(:key_mv, new_key).check!(eval_for(:key_mv, target_key: new_key))
|
|
58
58
|
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
|
|
59
59
|
|
|
60
60
|
[old_res, new_res]
|
data/lib/textus/write/reject.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.49.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -120,7 +120,6 @@ files:
|
|
|
120
120
|
- lib/textus/call.rb
|
|
121
121
|
- lib/textus/cli.rb
|
|
122
122
|
- lib/textus/cli/group.rb
|
|
123
|
-
- lib/textus/cli/group/fetch.rb
|
|
124
123
|
- lib/textus/cli/group/hook.rb
|
|
125
124
|
- lib/textus/cli/group/key.rb
|
|
126
125
|
- lib/textus/cli/group/mcp.rb
|
|
@@ -132,8 +131,6 @@ files:
|
|
|
132
131
|
- lib/textus/cli/verb/boot.rb
|
|
133
132
|
- lib/textus/cli/verb/build.rb
|
|
134
133
|
- lib/textus/cli/verb/doctor.rb
|
|
135
|
-
- lib/textus/cli/verb/fetch.rb
|
|
136
|
-
- lib/textus/cli/verb/fetch_all.rb
|
|
137
134
|
- lib/textus/cli/verb/get.rb
|
|
138
135
|
- lib/textus/cli/verb/hook_run.rb
|
|
139
136
|
- lib/textus/cli/verb/hooks.rb
|
|
@@ -157,10 +154,12 @@ files:
|
|
|
157
154
|
- lib/textus/doctor/check.rb
|
|
158
155
|
- lib/textus/doctor/check/audit_log.rb
|
|
159
156
|
- lib/textus/doctor/check/fetch_locks.rb
|
|
157
|
+
- lib/textus/doctor/check/generator_drift.rb
|
|
160
158
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
161
159
|
- lib/textus/doctor/check/hooks.rb
|
|
162
160
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
163
161
|
- lib/textus/doctor/check/intake_registration.rb
|
|
162
|
+
- lib/textus/doctor/check/lifecycle_action_invalid.rb
|
|
164
163
|
- lib/textus/doctor/check/manifest_files.rb
|
|
165
164
|
- lib/textus/doctor/check/orphaned_publish_targets.rb
|
|
166
165
|
- lib/textus/doctor/check/proposal_targets.rb
|
|
@@ -179,14 +178,15 @@ files:
|
|
|
179
178
|
- lib/textus/domain/freshness/evaluator.rb
|
|
180
179
|
- lib/textus/domain/freshness/policy.rb
|
|
181
180
|
- lib/textus/domain/freshness/verdict.rb
|
|
181
|
+
- lib/textus/domain/lifecycle.rb
|
|
182
182
|
- lib/textus/domain/outcome.rb
|
|
183
183
|
- lib/textus/domain/permission.rb
|
|
184
184
|
- lib/textus/domain/policy/base_guards.rb
|
|
185
185
|
- lib/textus/domain/policy/evaluation.rb
|
|
186
|
-
- lib/textus/domain/policy/fetch.rb
|
|
187
186
|
- lib/textus/domain/policy/guard.rb
|
|
188
187
|
- lib/textus/domain/policy/guard_factory.rb
|
|
189
188
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
189
|
+
- lib/textus/domain/policy/lifecycle.rb
|
|
190
190
|
- lib/textus/domain/policy/matcher.rb
|
|
191
191
|
- lib/textus/domain/policy/predicates/author_held.rb
|
|
192
192
|
- lib/textus/domain/policy/predicates/etag_match.rb
|
|
@@ -195,12 +195,9 @@ files:
|
|
|
195
195
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
196
196
|
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
197
197
|
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
198
|
-
- lib/textus/domain/policy/retention.rb
|
|
199
|
-
- lib/textus/domain/retention.rb
|
|
200
198
|
- lib/textus/domain/sentinel.rb
|
|
201
199
|
- lib/textus/domain/staleness.rb
|
|
202
200
|
- lib/textus/domain/staleness/generator_check.rb
|
|
203
|
-
- lib/textus/domain/staleness/intake_check.rb
|
|
204
201
|
- lib/textus/entry.rb
|
|
205
202
|
- lib/textus/entry/base.rb
|
|
206
203
|
- lib/textus/entry/json.rb
|
|
@@ -231,8 +228,8 @@ files:
|
|
|
231
228
|
- lib/textus/maintenance.rb
|
|
232
229
|
- lib/textus/maintenance/key_delete_prefix.rb
|
|
233
230
|
- lib/textus/maintenance/key_mv_prefix.rb
|
|
234
|
-
- lib/textus/maintenance/migrate.rb
|
|
235
231
|
- lib/textus/maintenance/rule_lint.rb
|
|
232
|
+
- lib/textus/maintenance/tend.rb
|
|
236
233
|
- lib/textus/maintenance/zone_mv.rb
|
|
237
234
|
- lib/textus/manifest.rb
|
|
238
235
|
- lib/textus/manifest/capabilities.rb
|
|
@@ -293,11 +290,9 @@ files:
|
|
|
293
290
|
- lib/textus/read/published.rb
|
|
294
291
|
- lib/textus/read/pulse.rb
|
|
295
292
|
- lib/textus/read/rdeps.rb
|
|
296
|
-
- lib/textus/read/retainable.rb
|
|
297
293
|
- lib/textus/read/rule_explain.rb
|
|
298
294
|
- lib/textus/read/rule_list.rb
|
|
299
295
|
- lib/textus/read/schema_envelope.rb
|
|
300
|
-
- lib/textus/read/stale.rb
|
|
301
296
|
- lib/textus/read/uid.rb
|
|
302
297
|
- lib/textus/read/validate_all.rb
|
|
303
298
|
- lib/textus/read/validator.rb
|
|
@@ -313,18 +308,16 @@ files:
|
|
|
313
308
|
- lib/textus/version.rb
|
|
314
309
|
- lib/textus/write/accept.rb
|
|
315
310
|
- lib/textus/write/build.rb
|
|
316
|
-
- lib/textus/write/delete.rb
|
|
317
|
-
- lib/textus/write/fetch_all.rb
|
|
318
311
|
- lib/textus/write/fetch_events.rb
|
|
319
312
|
- lib/textus/write/fetch_orchestrator.rb
|
|
320
313
|
- lib/textus/write/fetch_worker.rb
|
|
321
314
|
- lib/textus/write/intake_fetch.rb
|
|
315
|
+
- lib/textus/write/key_delete.rb
|
|
316
|
+
- lib/textus/write/key_mv.rb
|
|
322
317
|
- lib/textus/write/materializer.rb
|
|
323
|
-
- lib/textus/write/mv.rb
|
|
324
318
|
- lib/textus/write/propose.rb
|
|
325
319
|
- lib/textus/write/put.rb
|
|
326
320
|
- lib/textus/write/reject.rb
|
|
327
|
-
- lib/textus/write/retention_sweep.rb
|
|
328
321
|
homepage: https://github.com/patrick204nqh/textus
|
|
329
322
|
licenses:
|
|
330
323
|
- MIT
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Group
|
|
4
|
-
class Fetch < Group
|
|
5
|
-
command_name "fetch"
|
|
6
|
-
|
|
7
|
-
def parse(argv)
|
|
8
|
-
if argv.first == "all"
|
|
9
|
-
argv.shift
|
|
10
|
-
@sub_klass = Verb::FetchAll
|
|
11
|
-
else
|
|
12
|
-
@sub_klass = Verb::Fetch
|
|
13
|
-
end
|
|
14
|
-
@sub = @sub_klass.new(stdin: @stdin, stdout: @stdout, stderr: @stderr, cwd: @cwd)
|
|
15
|
-
@sub.parse(argv)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Fetch < Verb
|
|
5
|
-
option :as_flag, "--as=ROLE"
|
|
6
|
-
|
|
7
|
-
def call(store)
|
|
8
|
-
key = positional.shift or raise UsageError.new("fetch requires a key")
|
|
9
|
-
emit(session_for(store).fetch(key).to_h_for_wire)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class FetchAll < Verb
|
|
5
|
-
command_name "all"
|
|
6
|
-
parent_group Group::Fetch
|
|
7
|
-
|
|
8
|
-
option :prefix, "--prefix=KEY"
|
|
9
|
-
option :zone, "--zone=Z"
|
|
10
|
-
option :as_flag, "--as=ROLE"
|
|
11
|
-
|
|
12
|
-
def call(store)
|
|
13
|
-
result = session_for(store).fetch_all(prefix: prefix, zone: zone)
|
|
14
|
-
emit(result)
|
|
15
|
-
result["ok"] ? 0 : 1
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
class Fetch
|
|
5
|
-
ALLOWED_ON_STALE = %i[warn sync timed_sync].freeze
|
|
6
|
-
|
|
7
|
-
attr_reader :ttl, :on_stale, :sync_budget_ms, :fetch_timeout_seconds
|
|
8
|
-
|
|
9
|
-
def initialize(ttl:, on_stale:, sync_budget_ms:, fetch_timeout_seconds: nil)
|
|
10
|
-
on_stale_sym = on_stale.is_a?(Symbol) ? on_stale : on_stale.to_s.to_sym
|
|
11
|
-
unless ALLOWED_ON_STALE.include?(on_stale_sym)
|
|
12
|
-
raise Textus::UsageError.new(
|
|
13
|
-
"on_stale must be one of #{ALLOWED_ON_STALE.join(", ")} (got #{on_stale.inspect})",
|
|
14
|
-
)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
@ttl = ttl
|
|
18
|
-
@on_stale = on_stale_sym
|
|
19
|
-
@sync_budget_ms = sync_budget_ms
|
|
20
|
-
@fetch_timeout_seconds = fetch_timeout_seconds
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def ttl_seconds
|
|
24
|
-
Textus::Domain::Duration.seconds(@ttl)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def to_freshness_policy
|
|
28
|
-
Textus::Domain::Freshness::Policy.new(
|
|
29
|
-
ttl_seconds: ttl_seconds,
|
|
30
|
-
on_stale: @on_stale,
|
|
31
|
-
sync_budget_ms: @sync_budget_ms,
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
# Lifetime policy for queue/quarantine leaves. Both windows are optional
|
|
5
|
-
# durations (see Domain::Duration). `expire_after` deletes; `archive_after`
|
|
6
|
-
# moves the leaf aside. When both are set, expire wins once its (longer)
|
|
7
|
-
# window is exceeded.
|
|
8
|
-
class Retention
|
|
9
|
-
attr_reader :expire_after, :archive_after
|
|
10
|
-
|
|
11
|
-
def initialize(expire_after: nil, archive_after: nil)
|
|
12
|
-
@expire_after = Textus::Domain::Duration.seconds(expire_after)
|
|
13
|
-
@archive_after = Textus::Domain::Duration.seconds(archive_after)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# :expire | :archive | nil for a leaf of the given age (seconds).
|
|
17
|
-
def action_for(age_seconds)
|
|
18
|
-
return :expire if @expire_after && age_seconds > @expire_after
|
|
19
|
-
return :archive if @archive_after && age_seconds > @archive_after
|
|
20
|
-
|
|
21
|
-
nil
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
# Reports leaves whose age (now - file mtime) exceeds a retention window.
|
|
4
|
-
# Each row is { "key", "path", "action" => "expire"|"archive", "age_seconds" }.
|
|
5
|
-
class Retention
|
|
6
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
7
|
-
@manifest = manifest
|
|
8
|
-
@file_stat = file_stat
|
|
9
|
-
@clock = clock
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(prefix: nil, zone: nil)
|
|
13
|
-
@manifest.data.entries
|
|
14
|
-
.select { |m| entry_matches?(m, prefix: prefix, zone: zone) }
|
|
15
|
-
.flat_map { |m| rows_for(m) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def rows_for(mentry)
|
|
21
|
-
policy = @manifest.rules.for(mentry.key).retention
|
|
22
|
-
return [] if policy.nil?
|
|
23
|
-
|
|
24
|
-
@manifest.resolver.enumerate(prefix: mentry.key).filter_map do |row|
|
|
25
|
-
path = row[:path]
|
|
26
|
-
next unless @file_stat.exists?(path)
|
|
27
|
-
|
|
28
|
-
age = (@clock.now - @file_stat.mtime(path)).to_i
|
|
29
|
-
action = policy.action_for(age)
|
|
30
|
-
next if action.nil?
|
|
31
|
-
|
|
32
|
-
{ "key" => row[:key], "path" => path, "action" => action.to_s, "age_seconds" => age }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def entry_matches?(mentry, prefix:, zone:)
|
|
37
|
-
return false if zone && mentry.zone != zone
|
|
38
|
-
return false if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
|
|
39
|
-
|
|
40
|
-
true
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
class Staleness
|
|
6
|
-
# Reports TTL-exceeded staleness for intake-handler entries. Returns an
|
|
7
|
-
# Array of row hashes (possibly empty) per entry.
|
|
8
|
-
class IntakeCheck
|
|
9
|
-
def initialize(manifest:, file_stat:, clock:)
|
|
10
|
-
@manifest = manifest
|
|
11
|
-
@file_stat = file_stat
|
|
12
|
-
@clock = clock
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def rows_for(mentry)
|
|
16
|
-
return [] unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
17
|
-
|
|
18
|
-
ttl = @manifest.rules.for(mentry.key).fetch&.ttl_seconds
|
|
19
|
-
return [] unless ttl
|
|
20
|
-
|
|
21
|
-
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
22
|
-
reason = ttl_reason(mentry, path, ttl)
|
|
23
|
-
reason ? [row(mentry, path, reason)] : []
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def ttl_reason(mentry, path, ttl)
|
|
29
|
-
return "never fetched" unless @file_stat.exists?(path)
|
|
30
|
-
|
|
31
|
-
last_str = last_fetched_of(mentry, path)
|
|
32
|
-
return "never fetched (no last_fetched_at)" if last_str.nil?
|
|
33
|
-
|
|
34
|
-
last = parse_time(last_str)
|
|
35
|
-
"ttl exceeded (#{ttl}s)" if last.nil? || (@clock.now - last) > ttl
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def last_fetched_of(mentry, path)
|
|
39
|
-
Entry.for_format(mentry.format).parse(@file_stat.read(path), path: path)["_meta"]["last_fetched_at"]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def parse_time(str)
|
|
43
|
-
Time.parse(str.to_s)
|
|
44
|
-
rescue StandardError
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def row(mentry, path, reason)
|
|
49
|
-
{ "key" => mentry.key, "path" => path, "handler" => mentry.handler, "reason" => reason }
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|