textus 0.22.0 → 0.29.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 +195 -48
- data/CHANGELOG.md +178 -0
- data/README.md +55 -13
- data/SPEC.md +79 -42
- data/docs/conventions.md +10 -0
- data/lib/textus/boot.rb +31 -29
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/group/mcp.rb +9 -0
- data/lib/textus/cli/group/zone.rb +9 -0
- data/lib/textus/cli/verb/accept.rb +1 -1
- data/lib/textus/cli/verb/audit.rb +2 -2
- data/lib/textus/cli/verb/blame.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +3 -3
- data/lib/textus/cli/verb/delete.rb +1 -1
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -1
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +11 -14
- data/lib/textus/cli/verb/key_delete.rb +24 -0
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +17 -0
- data/lib/textus/cli/verb/migrate.rb +18 -0
- data/lib/textus/cli/verb/mv.rb +11 -3
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/pulse.rb +1 -1
- data/lib/textus/cli/verb/put.rb +8 -6
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +1 -1
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -1
- data/lib/textus/cli/verb/rule_lint.rb +18 -0
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb/zone_mv.rb +19 -0
- data/lib/textus/cli/verb.rb +7 -7
- data/lib/textus/cli.rb +0 -7
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +49 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
- data/lib/textus/doctor/check/hooks.rb +4 -3
- data/lib/textus/doctor/check/illegal_keys.rb +2 -2
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- data/lib/textus/doctor/check/manifest_files.rb +2 -2
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/refresh_locks.rb +2 -2
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/schemas.rb +2 -2
- data/lib/textus/doctor/check/sentinels.rb +11 -9
- data/lib/textus/doctor/check/templates.rb +2 -2
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +12 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +6 -6
- data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
- data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
- data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +20 -12
- data/lib/textus/domain/staleness.rb +4 -4
- data/lib/textus/envelope/io/reader.rb +44 -0
- data/lib/textus/{application/writes/envelope_io.rb → envelope/io/writer.rb} +32 -58
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/error_log.rb +32 -0
- data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
- data/lib/textus/hooks/loader.rb +29 -3
- data/lib/textus/hooks/rpc_registry.rb +77 -0
- data/lib/textus/key/path.rb +7 -3
- 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 +79 -0
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +8 -9
- 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 +2 -2
- 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 +48 -0
- data/lib/textus/manifest/resolver.rb +14 -14
- data/lib/textus/manifest/rules.rb +1 -1
- data/lib/textus/manifest.rb +47 -110
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +126 -0
- data/lib/textus/mcp/session.rb +40 -0
- data/lib/textus/mcp/tool_schemas.rb +71 -0
- data/lib/textus/mcp/tools.rb +129 -0
- data/lib/textus/mcp.rb +6 -0
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +7 -8
- 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 +37 -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/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 +49 -0
- data/lib/textus/schema/tools.rb +14 -10
- data/lib/textus/store.rb +25 -11
- 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 +54 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +123 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +59 -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 +138 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus.rb +7 -1
- metadata +75 -46
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/projection.rb +0 -91
- data/lib/textus/application/reads/audit.rb +0 -94
- data/lib/textus/application/reads/blame.rb +0 -82
- data/lib/textus/application/reads/deps.rb +0 -26
- data/lib/textus/application/reads/freshness.rb +0 -88
- data/lib/textus/application/reads/get.rb +0 -67
- data/lib/textus/application/reads/get_or_refresh.rb +0 -51
- data/lib/textus/application/reads/list.rb +0 -17
- data/lib/textus/application/reads/policy_explain.rb +0 -39
- data/lib/textus/application/reads/published.rb +0 -17
- data/lib/textus/application/reads/pulse.rb +0 -63
- data/lib/textus/application/reads/rdeps.rb +0 -27
- data/lib/textus/application/reads/schema_envelope.rb +0 -18
- data/lib/textus/application/reads/stale.rb +0 -15
- data/lib/textus/application/reads/uid.rb +0 -23
- data/lib/textus/application/reads/validate_all.rb +0 -24
- data/lib/textus/application/reads/validator.rb +0 -86
- data/lib/textus/application/reads/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/orchestrator.rb +0 -78
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/authority_gate.rb +0 -26
- data/lib/textus/application/writes/delete.rb +0 -33
- data/lib/textus/application/writes/materializer.rb +0 -50
- data/lib/textus/application/writes/mv.rb +0 -105
- data/lib/textus/application/writes/publish.rb +0 -81
- data/lib/textus/application/writes/put.rb +0 -37
- data/lib/textus/application/writes/reject.rb +0 -50
- data/lib/textus/infra/event_bus.rb +0 -27
- data/lib/textus/operations.rb +0 -176
|
@@ -16,22 +16,19 @@ module Textus
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
rows = []
|
|
19
|
-
Textus::Hooks::
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
30
|
-
rows << row
|
|
31
|
-
end
|
|
19
|
+
Textus::Hooks::RpcRegistry::EVENTS.each_key do |event|
|
|
20
|
+
store.rpc.names(event).each do |name|
|
|
21
|
+
rows << { "event" => event.to_s, "mode" => "rpc", "name" => name.to_s }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
Textus::Hooks::EventBus::EVENTS.each_key do |event|
|
|
25
|
+
store.events.pubsub_handlers(event).each do |h|
|
|
26
|
+
row = { "event" => event.to_s, "mode" => "pubsub", "name" => h[:name].to_s }
|
|
27
|
+
row["keys"] = Array(h[:keys]) if h[:keys]
|
|
28
|
+
rows << row
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
|
-
store.manifest.entries.each do |e|
|
|
31
|
+
store.manifest.data.entries.each do |e|
|
|
35
32
|
(e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
|
|
36
33
|
Array(defs).each do |defn|
|
|
37
34
|
next unless defn["exec"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class KeyDelete < Verb
|
|
5
|
+
command_name "delete"
|
|
6
|
+
parent_group Group::Key
|
|
7
|
+
|
|
8
|
+
option :as_flag, "--as=ROLE"
|
|
9
|
+
option :dry_run, "--dry-run"
|
|
10
|
+
option :prefix, "--prefix"
|
|
11
|
+
|
|
12
|
+
def call(store)
|
|
13
|
+
if prefix
|
|
14
|
+
p = positional.shift or raise UsageError.new("key delete --prefix requires <prefix>")
|
|
15
|
+
emit(session_for(store).key_delete_prefix(prefix: p, dry_run: dry_run || false).to_h)
|
|
16
|
+
else
|
|
17
|
+
key = positional.shift or raise UsageError.new("key delete requires <key>")
|
|
18
|
+
emit(session_for(store).delete(key))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/textus/cli/verb/list.rb
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
# Launches the MCP stdio server in the current process. Blocks on
|
|
5
|
+
# stdin; never returns until stdin closes.
|
|
6
|
+
class MCPServe < Verb
|
|
7
|
+
command_name "serve"
|
|
8
|
+
parent_group Group::MCP
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
Textus::MCP::Server.new(store: store, stdin: @stdin, stdout: @stdout).run
|
|
12
|
+
0
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Migrate < Verb
|
|
5
|
+
command_name "migrate"
|
|
6
|
+
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
option :dry_run, "--dry-run"
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
path = positional.shift or raise UsageError.new("migrate requires <plan.yaml>")
|
|
12
|
+
plan_yaml = File.read(path)
|
|
13
|
+
emit(session_for(store).migrate(plan_yaml: plan_yaml, dry_run: dry_run || false).to_h)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -7,11 +7,19 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
option :as_flag, "--as=ROLE"
|
|
9
9
|
option :dry_run, "--dry-run"
|
|
10
|
+
option :prefix, "--prefix"
|
|
10
11
|
|
|
11
12
|
def call(store)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if prefix
|
|
14
|
+
from_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
|
|
15
|
+
to_p = positional.shift or raise UsageError.new("mv --prefix requires <from-prefix> <to-prefix>")
|
|
16
|
+
emit(session_for(store).key_mv_prefix(from_prefix: from_p, to_prefix: to_p,
|
|
17
|
+
dry_run: dry_run || false).to_h)
|
|
18
|
+
else
|
|
19
|
+
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
20
|
+
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
21
|
+
emit(session_for(store).mv(old_key, new_key, dry_run: dry_run || false))
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
end
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
option :use_stdin, "--stdin"
|
|
9
9
|
option :fetch_name, "--fetch=NAME"
|
|
10
10
|
|
|
11
|
-
def call(store)
|
|
11
|
+
def call(store)
|
|
12
12
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
13
13
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
14
14
|
|
|
@@ -17,15 +17,17 @@ module Textus
|
|
|
17
17
|
raw = @stdin.read
|
|
18
18
|
payload =
|
|
19
19
|
if fetch_name
|
|
20
|
-
callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
|
|
21
20
|
result =
|
|
22
21
|
begin
|
|
23
|
-
Timeout.timeout(Textus::
|
|
24
|
-
|
|
22
|
+
Timeout.timeout(Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
|
|
23
|
+
store.rpc.invoke(:resolve_intake, fetch_name,
|
|
24
|
+
caps: nil,
|
|
25
|
+
config: { "bytes" => raw },
|
|
26
|
+
args: {})
|
|
25
27
|
end
|
|
26
28
|
rescue Timeout::Error
|
|
27
29
|
raise UsageError.new(
|
|
28
|
-
"fetch '#{fetch_name}' exceeded #{Textus::
|
|
30
|
+
"fetch '#{fetch_name}' exceeded #{Textus::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
29
31
|
)
|
|
30
32
|
end
|
|
31
33
|
basename = key.split(".").last
|
|
@@ -44,7 +46,7 @@ module Textus
|
|
|
44
46
|
meta = payload["_meta"] || {}
|
|
45
47
|
body = payload["body"] || ""
|
|
46
48
|
if_etag = payload["if_etag"]
|
|
47
|
-
result =
|
|
49
|
+
result = store.as(role).put(key, meta: meta, body: body, if_etag: if_etag)
|
|
48
50
|
emit(result.to_h_for_wire)
|
|
49
51
|
end
|
|
50
52
|
end
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
def call(store)
|
|
9
9
|
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
10
|
-
result =
|
|
10
|
+
result = session_for(store).policy_explain(key: key)
|
|
11
11
|
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
data/lib/textus/cli/verb/uid.rb
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -96,16 +96,16 @@ module Textus
|
|
|
96
96
|
Role.resolve(flag: flag, env: ENV, root: store.root)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# Returns
|
|
100
|
-
#
|
|
101
|
-
#
|
|
99
|
+
# Returns a Call value bound to the resolved role. Convenience for
|
|
100
|
+
# verbs whose only pre-call boilerplate is resolving the role and
|
|
101
|
+
# wrapping it in a Call.
|
|
102
102
|
def context_for(store)
|
|
103
|
-
Textus::
|
|
103
|
+
Textus::Call.build(role: resolved_role(store))
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Returns
|
|
107
|
-
def
|
|
108
|
-
|
|
106
|
+
# Returns a RoleScope bound to the resolved role.
|
|
107
|
+
def session_for(store)
|
|
108
|
+
store.as(resolved_role(store))
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
end
|
data/lib/textus/cli.rb
CHANGED
|
@@ -14,13 +14,6 @@ module Textus
|
|
|
14
14
|
.to_h { |k| [k.command_name, k] }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# Backward-compat constant; callers should prefer `CLI.verbs`.
|
|
18
|
-
def self.const_missing(name)
|
|
19
|
-
return verbs.freeze if name == :VERBS
|
|
20
|
-
|
|
21
|
-
super
|
|
22
|
-
end
|
|
23
|
-
|
|
24
17
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd)
|
|
25
18
|
new(stdin: stdin, stdout: stdout, stderr: stderr, cwd: cwd).run(argv)
|
|
26
19
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Single capability record handed to every use case. Replaces the
|
|
3
|
+
# ReadCaps/WriteCaps/HookCaps trio from 0.26.x. Built once per Store.
|
|
4
|
+
Container = Data.define(
|
|
5
|
+
:manifest, :file_store, :schemas, :root,
|
|
6
|
+
:audit_log, :events, :rpc, :authorizer
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
class Container
|
|
10
|
+
def self.from_store(store)
|
|
11
|
+
new(
|
|
12
|
+
manifest: store.manifest,
|
|
13
|
+
file_store: store.file_store,
|
|
14
|
+
schemas: store.schemas,
|
|
15
|
+
root: store.root,
|
|
16
|
+
audit_log: store.audit_log,
|
|
17
|
+
events: store.events,
|
|
18
|
+
rpc: store.rpc,
|
|
19
|
+
authorizer: Textus::Domain::Authorizer.new(manifest: store.manifest),
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Static verb → use-case map. Canonical lookup as of 0.27.0; replaces the
|
|
3
|
+
# Application::UseCase registry whose entries were populated by file-load
|
|
4
|
+
# side effects in 0.26.x.
|
|
5
|
+
module Dispatcher
|
|
6
|
+
VERBS = {
|
|
7
|
+
# Write
|
|
8
|
+
put: Textus::Write::Put,
|
|
9
|
+
delete: Textus::Write::Delete,
|
|
10
|
+
mv: Textus::Write::Mv,
|
|
11
|
+
accept: Textus::Write::Accept,
|
|
12
|
+
reject: Textus::Write::Reject,
|
|
13
|
+
publish: Textus::Write::Publish,
|
|
14
|
+
refresh: Textus::Write::RefreshWorker,
|
|
15
|
+
refresh_all: Textus::Write::RefreshAll,
|
|
16
|
+
|
|
17
|
+
# Read
|
|
18
|
+
get: Textus::Read::Get,
|
|
19
|
+
get_or_refresh: Textus::Read::GetOrRefresh,
|
|
20
|
+
list: Textus::Read::List,
|
|
21
|
+
where: Textus::Read::Where,
|
|
22
|
+
uid: Textus::Read::Uid,
|
|
23
|
+
blame: Textus::Read::Blame,
|
|
24
|
+
audit: Textus::Read::Audit,
|
|
25
|
+
freshness: Textus::Read::Freshness,
|
|
26
|
+
stale: Textus::Read::Stale,
|
|
27
|
+
deps: Textus::Read::Deps,
|
|
28
|
+
rdeps: Textus::Read::Rdeps,
|
|
29
|
+
pulse: Textus::Read::Pulse,
|
|
30
|
+
policy_explain: Textus::Read::PolicyExplain,
|
|
31
|
+
published: Textus::Read::Published,
|
|
32
|
+
schema_envelope: Textus::Read::SchemaEnvelope,
|
|
33
|
+
validate_all: Textus::Read::ValidateAll,
|
|
34
|
+
doctor: Textus::Read::Doctor,
|
|
35
|
+
boot: Textus::Read::Boot,
|
|
36
|
+
|
|
37
|
+
# Maintenance
|
|
38
|
+
migrate: Textus::Maintenance::Migrate,
|
|
39
|
+
zone_mv: Textus::Maintenance::ZoneMv,
|
|
40
|
+
key_mv_prefix: Textus::Maintenance::KeyMvPrefix,
|
|
41
|
+
key_delete_prefix: Textus::Maintenance::KeyDeletePrefix,
|
|
42
|
+
rule_lint: Textus::Maintenance::RuleLint,
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def self.fetch(verb)
|
|
46
|
+
VERBS.fetch(verb.to_sym) { raise UsageError.new("unknown verb: #{verb.inspect}") }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -3,8 +3,8 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
|
-
path = File.join(
|
|
7
|
-
Textus::
|
|
6
|
+
path = File.join(root, "audit.log")
|
|
7
|
+
Textus::Ports::AuditLog.new(root).verify_integrity.map do |v|
|
|
8
8
|
{
|
|
9
9
|
"code" => "audit.parse_error",
|
|
10
10
|
"level" => "warning",
|
|
@@ -7,12 +7,12 @@ module Textus
|
|
|
7
7
|
class HandlerAllowlist < Check
|
|
8
8
|
def call
|
|
9
9
|
out = []
|
|
10
|
-
|
|
10
|
+
manifest.data.entries.each do |mentry|
|
|
11
11
|
next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
12
12
|
|
|
13
13
|
handler = mentry.handler
|
|
14
14
|
|
|
15
|
-
allow =
|
|
15
|
+
allow = manifest.rules.for(mentry.key).handler_allowlist
|
|
16
16
|
next if allow.nil?
|
|
17
17
|
next if allow.allows?(handler)
|
|
18
18
|
|
|
@@ -4,15 +4,16 @@ module Textus
|
|
|
4
4
|
class Hooks < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
dir = File.join(
|
|
7
|
+
dir = File.join(root, "hooks")
|
|
8
8
|
return out unless File.directory?(dir)
|
|
9
9
|
|
|
10
10
|
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
-
|
|
11
|
+
events = Textus::Hooks::EventBus.new
|
|
12
|
+
rpc = Textus::Hooks::RpcRegistry.new
|
|
12
13
|
Textus.drain_hook_blocks
|
|
13
14
|
begin
|
|
14
15
|
load(f)
|
|
15
|
-
Textus.drain_hook_blocks.each { |b| b.call(
|
|
16
|
+
Textus.drain_hook_blocks.each { |b| b.call(Textus::Hooks::Loader::Dsl.new(events: events, rpc: rpc)) }
|
|
16
17
|
end
|
|
17
18
|
rescue StandardError, ScriptError => e
|
|
18
19
|
out << {
|
|
@@ -4,10 +4,10 @@ module Textus
|
|
|
4
4
|
class IllegalKeys < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
|
|
7
|
+
manifest.data.entries.each do |entry|
|
|
8
8
|
next unless entry.nested?
|
|
9
9
|
|
|
10
|
-
base = File.join(
|
|
10
|
+
base = File.join(root, "zones", entry.path)
|
|
11
11
|
next unless File.directory?(base)
|
|
12
12
|
|
|
13
13
|
index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
|
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered =
|
|
9
|
+
registered = rpc.names(:resolve_intake).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
@@ -35,7 +35,7 @@ module Textus
|
|
|
35
35
|
|
|
36
36
|
def collect_declared_handlers
|
|
37
37
|
set = Set.new
|
|
38
|
-
|
|
38
|
+
manifest.data.entries.each do |mentry|
|
|
39
39
|
set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
40
40
|
end
|
|
41
41
|
set
|
|
@@ -3,10 +3,10 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class ManifestFiles < Check
|
|
5
5
|
def call
|
|
6
|
-
|
|
6
|
+
manifest.data.entries.each_with_object([]) do |entry, out|
|
|
7
7
|
next if entry.nested?
|
|
8
8
|
|
|
9
|
-
path = Textus::Key::Path.resolve(
|
|
9
|
+
path = Textus::Key::Path.resolve(manifest.data, entry)
|
|
10
10
|
next if File.exist?(path)
|
|
11
11
|
|
|
12
12
|
out << {
|
|
@@ -23,10 +23,10 @@ module Textus
|
|
|
23
23
|
}]
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Doctor check interface:
|
|
26
|
+
# Doctor check interface: root is the .textus/ directory itself,
|
|
27
27
|
# so manifest.yaml lives directly inside it.
|
|
28
28
|
def call
|
|
29
|
-
path = File.join(
|
|
29
|
+
path = File.join(root, "manifest.yaml")
|
|
30
30
|
return [] unless File.exist?(path)
|
|
31
31
|
|
|
32
32
|
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
|
-
# Lists per-key refresh lock files under <
|
|
4
|
+
# Lists per-key refresh lock files under <root>/.locks/ whose
|
|
5
5
|
# recorded PID is no longer running. These are forensic artifacts only:
|
|
6
6
|
# Refresh::Lock uses flock(2), which the kernel releases on process
|
|
7
7
|
# death, so stale files do not block subsequent acquires. The check
|
|
@@ -9,7 +9,7 @@ module Textus
|
|
|
9
9
|
# (e.g. a refresh path that crashes repeatedly).
|
|
10
10
|
class RefreshLocks < Check
|
|
11
11
|
def call
|
|
12
|
-
dir = File.join(
|
|
12
|
+
dir = File.join(root, ".locks")
|
|
13
13
|
return [] unless File.directory?(dir)
|
|
14
14
|
|
|
15
15
|
Dir.glob(File.join(dir, "*.lock")).filter_map { |path| inspect_lock(path) }
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
# leaving the operator with no signal that a schema is broken.
|
|
8
8
|
class SchemaParseError < Check
|
|
9
9
|
def call
|
|
10
|
-
dir = File.join(
|
|
10
|
+
dir = File.join(root, "schemas")
|
|
11
11
|
return [] unless File.directory?(dir)
|
|
12
12
|
|
|
13
13
|
Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
res =
|
|
6
|
+
res = dispatch(:validate_all)
|
|
7
7
|
res["violations"].map do |v|
|
|
8
8
|
fix = v["expected"] &&
|
|
9
9
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
@@ -4,10 +4,10 @@ module Textus
|
|
|
4
4
|
class Schemas < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
|
|
7
|
+
manifest.data.entries.each do |entry|
|
|
8
8
|
next if entry.schema.nil?
|
|
9
9
|
|
|
10
|
-
sp = File.join(
|
|
10
|
+
sp = File.join(root, "schemas", "#{entry.schema}.yaml")
|
|
11
11
|
next if File.exist?(sp)
|
|
12
12
|
|
|
13
13
|
out << {
|
|
@@ -3,22 +3,24 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class Sentinels < Check
|
|
5
5
|
def call
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
store = Textus::Ports::SentinelStore.new
|
|
7
|
+
file_stat = Textus::Ports::Storage::FileStat.new
|
|
8
|
+
dir = File.join(root, "sentinels")
|
|
9
|
+
return [] unless file_stat.directory?(dir)
|
|
8
10
|
|
|
9
|
-
repo_root = File.dirname(
|
|
10
|
-
|
|
11
|
-
inspect_sentinel(sentinel_path, repo_root)
|
|
11
|
+
repo_root = File.dirname(root)
|
|
12
|
+
file_stat.glob(File.join(dir, "**", "*#{Textus::Ports::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
|
|
13
|
+
inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
private
|
|
16
18
|
|
|
17
|
-
def inspect_sentinel(sentinel_path, repo_root)
|
|
18
|
-
sentinel =
|
|
19
|
+
def inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
20
|
+
sentinel = store.load(sentinel_path, repo_root)
|
|
19
21
|
return [parse_error_issue(sentinel_path)] if sentinel.nil?
|
|
20
|
-
return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?
|
|
21
|
-
return [drift_issue(sentinel)] if sentinel.drift?
|
|
22
|
+
return [orphan_issue(sentinel_path, sentinel)] if sentinel.orphan?(file_stat)
|
|
23
|
+
return [drift_issue(sentinel)] if sentinel.drift?(file_stat)
|
|
22
24
|
|
|
23
25
|
[]
|
|
24
26
|
end
|