textus 0.43.1 → 0.45.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/CHANGELOG.md +70 -0
- data/README.md +3 -3
- data/SPEC.md +15 -13
- data/docs/architecture/README.md +28 -9
- data/docs/reference/conventions.md +8 -9
- data/lib/textus/boot.rb +10 -7
- data/lib/textus/cli/group/fetch.rb +2 -2
- data/lib/textus/cli/group.rb +1 -0
- data/lib/textus/cli/runner.rb +187 -0
- data/lib/textus/cli/verb/build.rb +4 -4
- data/lib/textus/cli/verb/{fetch_stale.rb → fetch_all.rb} +2 -2
- data/lib/textus/cli/verb/get.rb +6 -5
- data/lib/textus/cli/verb/put.rb +3 -3
- data/lib/textus/cli/verb.rb +3 -0
- data/lib/textus/cli.rb +8 -2
- data/lib/textus/contract/around.rb +29 -0
- data/lib/textus/contract/binder.rb +88 -0
- data/lib/textus/contract/resources/cursor.rb +26 -0
- data/lib/textus/contract/sources.rb +39 -0
- data/lib/textus/contract/view.rb +15 -0
- data/lib/textus/contract.rb +78 -10
- data/lib/textus/dispatcher.rb +5 -6
- data/lib/textus/hooks/context.rb +24 -2
- data/lib/textus/maintenance/key_delete_prefix.rb +6 -4
- data/lib/textus/maintenance/key_mv_prefix.rb +7 -5
- data/lib/textus/maintenance/migrate.rb +12 -8
- data/lib/textus/maintenance/rule_lint.rb +4 -2
- data/lib/textus/maintenance/zone_mv.rb +7 -5
- data/lib/textus/manifest/entry/base.rb +1 -1
- data/lib/textus/mcp/catalog.rb +17 -33
- data/lib/textus/projection.rb +2 -2
- data/lib/textus/read/audit.rb +19 -0
- data/lib/textus/read/blame.rb +11 -1
- data/lib/textus/read/deps.rb +15 -1
- data/lib/textus/read/doctor.rb +8 -0
- data/lib/textus/read/freshness.rb +10 -0
- data/lib/textus/read/get.rb +88 -22
- data/lib/textus/read/list.rb +3 -2
- data/lib/textus/read/published.rb +7 -0
- data/lib/textus/read/pulse.rb +1 -0
- data/lib/textus/read/rdeps.rb +14 -0
- data/lib/textus/read/rule_explain.rb +84 -0
- data/lib/textus/read/rule_list.rb +39 -0
- data/lib/textus/read/schema_envelope.rb +5 -3
- data/lib/textus/read/uid.rb +9 -0
- data/lib/textus/read/where.rb +8 -0
- data/lib/textus/role_scope.rb +34 -6
- data/lib/textus/schema/tools.rb +12 -3
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +8 -0
- data/lib/textus/write/{publish.rb → build.rb} +16 -7
- data/lib/textus/write/delete.rb +13 -0
- data/lib/textus/write/fetch_all.rb +3 -2
- data/lib/textus/write/fetch_orchestrator.rb +1 -1
- data/lib/textus/write/fetch_worker.rb +3 -2
- data/lib/textus/write/mv.rb +16 -0
- data/lib/textus/write/propose.rb +12 -4
- data/lib/textus/write/put.rb +11 -6
- data/lib/textus/write/reject.rb +8 -0
- data/lib/textus/write/retention_sweep.rb +9 -0
- metadata +11 -29
- data/lib/textus/cli/verb/accept.rb +0 -16
- data/lib/textus/cli/verb/audit.rb +0 -34
- data/lib/textus/cli/verb/blame.rb +0 -17
- data/lib/textus/cli/verb/delete.rb +0 -17
- data/lib/textus/cli/verb/deps.rb +0 -14
- data/lib/textus/cli/verb/freshness.rb +0 -17
- data/lib/textus/cli/verb/key_delete.rb +0 -24
- data/lib/textus/cli/verb/list.rb +0 -16
- data/lib/textus/cli/verb/migrate.rb +0 -18
- data/lib/textus/cli/verb/mv.rb +0 -27
- data/lib/textus/cli/verb/propose.rb +0 -28
- data/lib/textus/cli/verb/published.rb +0 -13
- data/lib/textus/cli/verb/pulse.rb +0 -26
- data/lib/textus/cli/verb/rdeps.rb +0 -14
- data/lib/textus/cli/verb/reject.rb +0 -16
- data/lib/textus/cli/verb/retain.rb +0 -19
- data/lib/textus/cli/verb/rule_explain.rb +0 -16
- data/lib/textus/cli/verb/rule_lint.rb +0 -18
- data/lib/textus/cli/verb/rule_list.rb +0 -29
- data/lib/textus/cli/verb/schema.rb +0 -15
- data/lib/textus/cli/verb/uid.rb +0 -15
- data/lib/textus/cli/verb/where.rb +0 -14
- data/lib/textus/cli/verb/zone_mv.rb +0 -19
- data/lib/textus/read/get_or_fetch.rb +0 -69
- data/lib/textus/read/policy_explain.rb +0 -46
- data/lib/textus/read/rules.rb +0 -24
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Reject < Verb
|
|
5
|
-
command_name "reject"
|
|
6
|
-
|
|
7
|
-
option :as_flag, "--as=ROLE"
|
|
8
|
-
|
|
9
|
-
def call(store)
|
|
10
|
-
key = positional.shift or raise UsageError.new("reject requires a key")
|
|
11
|
-
emit(session_for(store).reject(key))
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Retain < Verb
|
|
5
|
-
command_name "retain"
|
|
6
|
-
|
|
7
|
-
option :prefix, "--prefix=KEY"
|
|
8
|
-
option :zone, "--zone=Z"
|
|
9
|
-
option :as_flag, "--as=ROLE"
|
|
10
|
-
|
|
11
|
-
def call(store)
|
|
12
|
-
result = session_for(store).retention_sweep(prefix: prefix, zone: zone)
|
|
13
|
-
emit(result)
|
|
14
|
-
result["ok"] ? 0 : 1
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class RuleExplain < Verb
|
|
5
|
-
command_name "explain"
|
|
6
|
-
parent_group Group::Rule
|
|
7
|
-
|
|
8
|
-
def call(store)
|
|
9
|
-
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
10
|
-
result = session_for(store).policy_explain(key: key)
|
|
11
|
-
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class RuleLint < Verb
|
|
5
|
-
command_name "lint"
|
|
6
|
-
parent_group Group::Rule
|
|
7
|
-
|
|
8
|
-
option :against, "--against=FILE"
|
|
9
|
-
|
|
10
|
-
def call(store)
|
|
11
|
-
path = against or raise UsageError.new("rule lint --against=FILE required")
|
|
12
|
-
yaml = File.read(path)
|
|
13
|
-
emit(session_for(store).rule_lint(candidate_yaml: yaml).to_h)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class RuleList < Verb
|
|
5
|
-
command_name "list"
|
|
6
|
-
parent_group Group::Rule
|
|
7
|
-
|
|
8
|
-
def call(store)
|
|
9
|
-
policies = store.manifest.rules.blocks.map do |b|
|
|
10
|
-
row = { "match" => b.match }
|
|
11
|
-
if b.fetch
|
|
12
|
-
row["fetch"] = {
|
|
13
|
-
"ttl_seconds" => b.fetch.ttl_seconds,
|
|
14
|
-
"on_stale" => b.fetch.on_stale,
|
|
15
|
-
"sync_budget_ms" => b.fetch.sync_budget_ms,
|
|
16
|
-
"fetch_timeout_seconds" => b.fetch.fetch_timeout_seconds,
|
|
17
|
-
}
|
|
18
|
-
end
|
|
19
|
-
row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
|
|
20
|
-
row["guard"] = b.guard if b.guard
|
|
21
|
-
row["retention"] = { "expire_after" => b.retention.expire_after, "archive_after" => b.retention.archive_after } if b.retention
|
|
22
|
-
row
|
|
23
|
-
end
|
|
24
|
-
emit({ "verb" => "policy_list", "policies" => policies })
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Schema < Verb
|
|
5
|
-
command_name "show"
|
|
6
|
-
parent_group Group::Schema
|
|
7
|
-
|
|
8
|
-
def call(store)
|
|
9
|
-
key = positional.shift or raise UsageError.new("schema requires a key")
|
|
10
|
-
emit(session_for(store).schema(key))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/textus/cli/verb/uid.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Uid < Verb
|
|
5
|
-
command_name "uid"
|
|
6
|
-
parent_group Group::Key
|
|
7
|
-
|
|
8
|
-
def call(store)
|
|
9
|
-
key = positional.shift or raise UsageError.new("uid requires a key")
|
|
10
|
-
emit({ "key" => key, "uid" => session_for(store).uid(key) })
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class ZoneMv < Verb
|
|
5
|
-
command_name "mv"
|
|
6
|
-
parent_group Group::Zone
|
|
7
|
-
|
|
8
|
-
option :as_flag, "--as=ROLE"
|
|
9
|
-
option :dry_run, "--dry-run"
|
|
10
|
-
|
|
11
|
-
def call(store)
|
|
12
|
-
from = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
|
|
13
|
-
to = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
|
|
14
|
-
emit(session_for(store).zone_mv(from: from, to: to, dry_run: dry_run || false).to_h)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Composes pure `Read::Get` with the fetch orchestrator: runs Get
|
|
4
|
-
# to obtain the envelope and freshness verdict, then if the verdict
|
|
5
|
-
# is stale and the rule's `on_stale` policy demands action, hands
|
|
6
|
-
# off to the orchestrator. Use for interactive reads where the
|
|
7
|
-
# caller wants the freshest obtainable envelope.
|
|
8
|
-
#
|
|
9
|
-
# Pure reads (build, projection, schema tooling) should use
|
|
10
|
-
# `Read::Get` directly; it has no orchestrator dependency.
|
|
11
|
-
class GetOrFetch
|
|
12
|
-
def initialize(container:, call:, get: nil, orchestrator: nil)
|
|
13
|
-
@container = container
|
|
14
|
-
@call = call
|
|
15
|
-
@manifest = container.manifest
|
|
16
|
-
@get = get || Read::Get.new(container: container, call: call)
|
|
17
|
-
@orchestrator = orchestrator || build_orchestrator
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def hook_context
|
|
23
|
-
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def build_orchestrator
|
|
27
|
-
worker = Textus::Write::FetchWorker.new(
|
|
28
|
-
container: @container, call: @call,
|
|
29
|
-
)
|
|
30
|
-
Textus::Write::FetchOrchestrator.new(
|
|
31
|
-
worker: worker, store_root: @container.root, events: @container.events,
|
|
32
|
-
hook_context: hook_context
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
public
|
|
37
|
-
|
|
38
|
-
def call(key)
|
|
39
|
-
envelope = @get.call(key)
|
|
40
|
-
return nil if envelope.nil?
|
|
41
|
-
return envelope unless envelope.freshness&.stale
|
|
42
|
-
|
|
43
|
-
policy_set = @manifest.rules.for(key)
|
|
44
|
-
fetch_policy = policy_set.fetch
|
|
45
|
-
return envelope if fetch_policy.nil?
|
|
46
|
-
|
|
47
|
-
policy = fetch_policy.to_freshness_policy
|
|
48
|
-
verdict = Textus::Domain::Freshness::Verdict.stale(envelope.freshness.reason)
|
|
49
|
-
action = policy.decide(verdict)
|
|
50
|
-
outcome = @orchestrator.execute(action, key: key)
|
|
51
|
-
|
|
52
|
-
case outcome
|
|
53
|
-
when Textus::Domain::Outcome::Skipped
|
|
54
|
-
envelope
|
|
55
|
-
when Textus::Domain::Outcome::Fetched
|
|
56
|
-
outcome.envelope.with(
|
|
57
|
-
freshness: Textus::Domain::Freshness.build(stale: false, reason: nil, fetching: false),
|
|
58
|
-
)
|
|
59
|
-
when Textus::Domain::Outcome::Detached
|
|
60
|
-
envelope.with(freshness: envelope.freshness.with(fetching: true))
|
|
61
|
-
when Textus::Domain::Outcome::Failed
|
|
62
|
-
envelope.with(
|
|
63
|
-
freshness: envelope.freshness.with(fetch_error: outcome.error.message),
|
|
64
|
-
)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# For one key, surface every matching policy block along with the
|
|
4
|
-
# per-slot effective value (which loses ties win-by-specificity) and the
|
|
5
|
-
# effective guard predicate names for every write transition (ADR 0031).
|
|
6
|
-
class PolicyExplain
|
|
7
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
-
@manifest = container.manifest
|
|
9
|
-
@schemas = container.schemas
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def call(key:)
|
|
13
|
-
matching = @manifest.rules.explain(key)
|
|
14
|
-
winners = @manifest.rules.for(key)
|
|
15
|
-
factory = Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
key: key,
|
|
19
|
-
matched_blocks: matching.map do |b|
|
|
20
|
-
{
|
|
21
|
-
match: b.match,
|
|
22
|
-
fetch: !b.fetch.nil?,
|
|
23
|
-
handler_allowlist: !b.handler_allowlist.nil?,
|
|
24
|
-
guard: !b.guard.nil?,
|
|
25
|
-
retention: !b.retention.nil?,
|
|
26
|
-
}
|
|
27
|
-
end,
|
|
28
|
-
effective: {
|
|
29
|
-
fetch: winners.fetch && {
|
|
30
|
-
ttl_seconds: winners.fetch.ttl_seconds,
|
|
31
|
-
on_stale: winners.fetch.on_stale,
|
|
32
|
-
},
|
|
33
|
-
handler_allowlist: winners.handler_allowlist&.handlers,
|
|
34
|
-
retention: winners.retention && {
|
|
35
|
-
expire_after: winners.retention.expire_after,
|
|
36
|
-
archive_after: winners.retention.archive_after,
|
|
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,
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
data/lib/textus/read/rules.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Read
|
|
3
|
-
# Effective rule set (fetch + guard) for a key. Was the inlined MCP
|
|
4
|
-
# `rules` tool; promoted to a first-class verb so MCP is a pure projection
|
|
5
|
-
# (ADR 0039).
|
|
6
|
-
class Rules
|
|
7
|
-
extend Textus::Contract::DSL
|
|
8
|
-
|
|
9
|
-
verb :rules
|
|
10
|
-
summary "Return effective rules for a key (fetch, guard, ...)."
|
|
11
|
-
surfaces :ruby, :mcp
|
|
12
|
-
arg :key, String, required: true, positional: true
|
|
13
|
-
|
|
14
|
-
def initialize(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
-
@manifest = container.manifest
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def call(key)
|
|
19
|
-
set = @manifest.rules.for(key)
|
|
20
|
-
{ "fetch" => set.fetch&.to_h, "guard" => set.guard }.compact
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|