textus 0.22.0 → 0.26.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 +148 -45
- data/CHANGELOG.md +102 -0
- data/README.md +1 -1
- data/SPEC.md +12 -12
- data/docs/conventions.md +10 -0
- data/lib/textus/application/caps.rb +49 -0
- data/lib/textus/application/context.rb +2 -2
- data/lib/textus/application/envelope/reader.rb +44 -0
- data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
- data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
- data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
- data/lib/textus/application/maintenance/migrate.rb +59 -0
- data/lib/textus/application/maintenance/rule_lint.rb +65 -0
- data/lib/textus/application/maintenance/zone_mv.rb +60 -0
- data/lib/textus/application/maintenance.rb +17 -0
- data/lib/textus/application/projection.rb +12 -10
- data/lib/textus/application/read/audit.rb +106 -0
- data/lib/textus/application/read/blame.rb +91 -0
- data/lib/textus/application/read/deps.rb +34 -0
- data/lib/textus/application/read/freshness.rb +110 -0
- data/lib/textus/application/read/get.rb +75 -0
- data/lib/textus/application/read/get_or_refresh.rb +63 -0
- data/lib/textus/application/read/list.rb +25 -0
- data/lib/textus/application/read/policy_explain.rb +47 -0
- data/lib/textus/application/read/published.rb +25 -0
- data/lib/textus/application/read/pulse.rb +101 -0
- data/lib/textus/application/read/rdeps.rb +35 -0
- data/lib/textus/application/read/schema_envelope.rb +26 -0
- data/lib/textus/application/read/stale.rb +23 -0
- data/lib/textus/application/read/uid.rb +30 -0
- data/lib/textus/application/read/validate_all.rb +32 -0
- data/lib/textus/application/{reads → read}/validator.rb +2 -2
- data/lib/textus/application/read/where.rb +26 -0
- data/lib/textus/application/use_case.rb +22 -0
- data/lib/textus/application/write/accept.rb +102 -0
- data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
- data/lib/textus/application/write/delete.rb +45 -0
- data/lib/textus/application/{writes → write}/materializer.rb +14 -15
- data/lib/textus/application/write/mv.rb +118 -0
- data/lib/textus/application/write/publish.rb +96 -0
- data/lib/textus/application/write/put.rb +49 -0
- data/lib/textus/application/write/refresh_all.rb +63 -0
- data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
- data/lib/textus/application/write/refresh_worker.rb +134 -0
- data/lib/textus/application/write/reject.rb +62 -0
- data/lib/textus/boot.rb +27 -29
- data/lib/textus/builder/pipeline.rb +3 -3
- 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 +2 -2
- 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 +4 -4
- 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 +2 -2
- 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 +5 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +4 -4
- 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/staleness/generator_check.rb +2 -2
- data/lib/textus/domain/staleness/intake_check.rb +2 -2
- data/lib/textus/domain/staleness.rb +1 -1
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +13 -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/infra/audit_subscriber.rb +6 -7
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/key/path.rb +7 -3
- data/lib/textus/manifest/data.rb +78 -0
- data/lib/textus/manifest/entry/base.rb +4 -4
- data/lib/textus/manifest/entry/derived.rb +4 -5
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- 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 +53 -111
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +127 -0
- data/lib/textus/mcp/session.rb +31 -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/schema/tools.rb +14 -10
- data/lib/textus/session.rb +84 -0
- data/lib/textus/store.rb +14 -9
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +8 -1
- metadata +61 -36
- 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/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/delete.rb +0 -33
- 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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module MCP
|
|
3
|
+
# Dispatch table for MCP tool names → implementations. Each implementation
|
|
4
|
+
# receives (session:, store:, args:) and returns a JSON-encodable value.
|
|
5
|
+
# Tool errors are wrapped in ToolError; ContractDrift / CursorExpired
|
|
6
|
+
# propagate verbatim so the server can map them to JSON-RPC codes.
|
|
7
|
+
module Tools
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(name, session:, store:, args:)
|
|
11
|
+
impl = REGISTRY[name] or raise ToolError.new("unknown tool: #{name}")
|
|
12
|
+
impl.call(session, store, args || {})
|
|
13
|
+
rescue ContractDrift, CursorExpired
|
|
14
|
+
raise
|
|
15
|
+
rescue Textus::Error => e
|
|
16
|
+
raise ToolError.new("#{name}: #{e.message}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ops_for(session, store)
|
|
20
|
+
store.session(role: session.role)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
REGISTRY = {
|
|
24
|
+
"boot" => ->(_s, store, _a) { Textus::Boot.run(Textus::Session.for(store)) },
|
|
25
|
+
|
|
26
|
+
"find" => lambda do |s, store, args|
|
|
27
|
+
ops_for(s, store).list(zone: args["zone"], prefix: args["prefix"])
|
|
28
|
+
end,
|
|
29
|
+
|
|
30
|
+
"read" => lambda do |s, store, args|
|
|
31
|
+
key = args.fetch("key") { raise ToolError.new("read: missing key") }
|
|
32
|
+
env = ops_for(s, store).get(key)
|
|
33
|
+
env.to_h_for_wire
|
|
34
|
+
end,
|
|
35
|
+
|
|
36
|
+
"tick" => lambda do |s, store, args|
|
|
37
|
+
since = (args["since"] || s.cursor).to_i
|
|
38
|
+
ops_for(s, store).pulse(since: since)
|
|
39
|
+
end,
|
|
40
|
+
|
|
41
|
+
"write" => lambda do |s, store, args|
|
|
42
|
+
key = args.fetch("key") { raise ToolError.new("write: missing key") }
|
|
43
|
+
env = ops_for(s, store).put(
|
|
44
|
+
key,
|
|
45
|
+
meta: args["meta"] || {},
|
|
46
|
+
body: args["body"],
|
|
47
|
+
content: args["content"],
|
|
48
|
+
if_etag: args["if_etag"],
|
|
49
|
+
)
|
|
50
|
+
{ "uid" => env.uid, "etag" => env.etag }
|
|
51
|
+
end,
|
|
52
|
+
|
|
53
|
+
"propose" => lambda do |s, store, args|
|
|
54
|
+
raise ToolError.new("propose: session has no propose_zone") unless s.propose_zone
|
|
55
|
+
|
|
56
|
+
rel = args.fetch("key") { raise ToolError.new("propose: missing key") }
|
|
57
|
+
target = "#{s.propose_zone}.#{rel}"
|
|
58
|
+
env = ops_for(s, store).put(
|
|
59
|
+
target,
|
|
60
|
+
meta: args["meta"] || {},
|
|
61
|
+
body: args["body"],
|
|
62
|
+
content: args["content"],
|
|
63
|
+
)
|
|
64
|
+
{ "uid" => env.uid, "etag" => env.etag, "key" => target }
|
|
65
|
+
end,
|
|
66
|
+
|
|
67
|
+
"refresh" => lambda do |s, store, args|
|
|
68
|
+
key = args.fetch("key") { raise ToolError.new("refresh: missing key") }
|
|
69
|
+
outcome = ops_for(s, store).refresh(key)
|
|
70
|
+
{ "outcome" => outcome.class.name.split("::").last.downcase }
|
|
71
|
+
end,
|
|
72
|
+
|
|
73
|
+
"refresh_stale" => lambda do |s, store, args|
|
|
74
|
+
ops_for(s, store).refresh_all(zone: args["zone"], prefix: args["prefix"])
|
|
75
|
+
end,
|
|
76
|
+
|
|
77
|
+
"schema" => lambda do |_s, store, args|
|
|
78
|
+
family = args.fetch("family") { raise ToolError.new("schema: missing family") }
|
|
79
|
+
store.schemas.fetch(family)
|
|
80
|
+
end,
|
|
81
|
+
|
|
82
|
+
"rules" => lambda do |_s, store, args|
|
|
83
|
+
key = args.fetch("key") { raise ToolError.new("rules: missing key") }
|
|
84
|
+
set = store.manifest.rules.for(key)
|
|
85
|
+
{
|
|
86
|
+
"refresh" => set.refresh&.to_h,
|
|
87
|
+
"promote" => set.respond_to?(:promote) ? set.promote&.to_h : nil,
|
|
88
|
+
}.compact
|
|
89
|
+
end,
|
|
90
|
+
|
|
91
|
+
"key_mv_prefix" => lambda do |s, store, args|
|
|
92
|
+
ops_for(s, store).key_mv_prefix(
|
|
93
|
+
from_prefix: args.fetch("from_prefix") { raise ToolError.new("key_mv_prefix: missing from_prefix") },
|
|
94
|
+
to_prefix: args.fetch("to_prefix") { raise ToolError.new("key_mv_prefix: missing to_prefix") },
|
|
95
|
+
dry_run: args["dry_run"] || false,
|
|
96
|
+
).to_h
|
|
97
|
+
end,
|
|
98
|
+
|
|
99
|
+
"key_delete_prefix" => lambda do |s, store, args|
|
|
100
|
+
ops_for(s, store).key_delete_prefix(
|
|
101
|
+
prefix: args.fetch("prefix") { raise ToolError.new("key_delete_prefix: missing prefix") },
|
|
102
|
+
dry_run: args["dry_run"] || false,
|
|
103
|
+
).to_h
|
|
104
|
+
end,
|
|
105
|
+
|
|
106
|
+
"zone_mv" => lambda do |s, store, args|
|
|
107
|
+
ops_for(s, store).zone_mv(
|
|
108
|
+
from: args.fetch("from") { raise ToolError.new("zone_mv: missing from") },
|
|
109
|
+
to: args.fetch("to") { raise ToolError.new("zone_mv: missing to") },
|
|
110
|
+
dry_run: args["dry_run"] || false,
|
|
111
|
+
).to_h
|
|
112
|
+
end,
|
|
113
|
+
|
|
114
|
+
"rule_lint" => lambda do |s, store, args|
|
|
115
|
+
ops_for(s, store).rule_lint(
|
|
116
|
+
candidate_yaml: args.fetch("candidate_yaml") { raise ToolError.new("rule_lint: missing candidate_yaml") },
|
|
117
|
+
).to_h
|
|
118
|
+
end,
|
|
119
|
+
|
|
120
|
+
"migrate" => lambda do |s, store, args|
|
|
121
|
+
ops_for(s, store).migrate(
|
|
122
|
+
plan_yaml: args.fetch("plan_yaml") { raise ToolError.new("migrate: missing plan_yaml") },
|
|
123
|
+
dry_run: args["dry_run"] || false,
|
|
124
|
+
).to_h
|
|
125
|
+
end,
|
|
126
|
+
}.freeze
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/textus/mcp.rb
ADDED
data/lib/textus/schema/tools.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
module Tools
|
|
7
7
|
# textus schema init NAME --from=KEY → infer YAML schema from an entry's frontmatter
|
|
8
8
|
def self.init(store, name:, from:)
|
|
9
|
-
env =
|
|
9
|
+
env = store.session.get(from)
|
|
10
10
|
meta = env.meta
|
|
11
11
|
schema = {
|
|
12
12
|
"name" => name,
|
|
@@ -25,7 +25,7 @@ module Textus
|
|
|
25
25
|
schema = load_schema(store, name)
|
|
26
26
|
drift = []
|
|
27
27
|
store.manifest.resolver.enumerate.each do |row|
|
|
28
|
-
env =
|
|
28
|
+
env = store.session.get(row[:key])
|
|
29
29
|
begin
|
|
30
30
|
schema.validate!(env.meta)
|
|
31
31
|
rescue SchemaViolation => e
|
|
@@ -49,14 +49,8 @@ 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 = store
|
|
53
|
-
|
|
54
|
-
raise UsageError.new(
|
|
55
|
-
"schema migrate requires a role with kind :accept_authority in the manifest; " \
|
|
56
|
-
"none declared (add e.g. `- { name: owner, kind: accept_authority }` to roles:)",
|
|
57
|
-
)
|
|
58
|
-
end
|
|
59
|
-
ops = Textus::Operations.for(store, role: authority)
|
|
52
|
+
authority = accept_authority_for(store)
|
|
53
|
+
ops = store.session(role: authority)
|
|
60
54
|
touched = []
|
|
61
55
|
store.manifest.resolver.enumerate.each do |row|
|
|
62
56
|
env = ops.get(row[:key])
|
|
@@ -92,6 +86,16 @@ module Textus
|
|
|
92
86
|
rescue IoError
|
|
93
87
|
raise UsageError.new("schema not found: #{name}")
|
|
94
88
|
end
|
|
89
|
+
|
|
90
|
+
def self.accept_authority_for(store)
|
|
91
|
+
authority = store.manifest.policy.roles_with_kind(:accept_authority).first
|
|
92
|
+
return authority if authority
|
|
93
|
+
|
|
94
|
+
raise UsageError.new(
|
|
95
|
+
"schema migrate requires a role with kind :accept_authority in the manifest; " \
|
|
96
|
+
"none declared (add e.g. `- { name: owner, kind: accept_authority }` to roles:)",
|
|
97
|
+
)
|
|
98
|
+
end
|
|
95
99
|
end
|
|
96
100
|
end
|
|
97
101
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Per-call session. Holds ctx (role, correlation_id, now, dry_run) and
|
|
3
|
+
# the three caps records. Generates one method per registered use case.
|
|
4
|
+
class Session
|
|
5
|
+
attr_reader :ctx, :read_caps, :write_caps, :hook_caps
|
|
6
|
+
|
|
7
|
+
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
8
|
+
read_caps, write_caps, hook_caps = Application.caps_from_store(store)
|
|
9
|
+
new(
|
|
10
|
+
ctx: Application::Context.build(role: role, correlation_id: correlation_id, dry_run: dry_run),
|
|
11
|
+
read_caps: read_caps, write_caps: write_caps, hook_caps: hook_caps
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(ctx:, read_caps:, write_caps:, hook_caps:)
|
|
16
|
+
@ctx = ctx
|
|
17
|
+
@read_caps = read_caps
|
|
18
|
+
@write_caps = write_caps
|
|
19
|
+
@hook_caps = hook_caps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_role(role)
|
|
23
|
+
self.class.new(
|
|
24
|
+
ctx: @ctx.with_role(role),
|
|
25
|
+
read_caps: @read_caps, write_caps: @write_caps, hook_caps: @hook_caps
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hook_context
|
|
30
|
+
@hook_context ||= Hooks::Context.new(session: self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def rpc = @hook_caps.rpc
|
|
34
|
+
def events = @hook_caps.events
|
|
35
|
+
|
|
36
|
+
def envelope_reader
|
|
37
|
+
@envelope_reader ||= Application::Envelope::Reader.new(
|
|
38
|
+
file_store: @read_caps.file_store, manifest: @read_caps.manifest,
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def envelope_writer
|
|
43
|
+
@envelope_writer ||= Application::Envelope::Writer.new(
|
|
44
|
+
file_store: @write_caps.file_store, manifest: @write_caps.manifest,
|
|
45
|
+
schemas: @write_caps.schemas, audit_log: @write_caps.audit_log,
|
|
46
|
+
ctx: @ctx, reader: envelope_reader
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def boot(...) = Textus::Boot.run(self, ...)
|
|
51
|
+
def doctor(...) = Textus::Doctor.run(self, ...)
|
|
52
|
+
|
|
53
|
+
def refresh_orchestrator
|
|
54
|
+
@refresh_orchestrator ||= Application::Write::RefreshOrchestrator.new(
|
|
55
|
+
worker: refresh_worker,
|
|
56
|
+
store_root: @write_caps.root,
|
|
57
|
+
events: @write_caps.events,
|
|
58
|
+
ctx: @ctx,
|
|
59
|
+
hook_context: hook_context,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def refresh_worker
|
|
64
|
+
@refresh_worker ||= Application::Write::RefreshWorker::Impl.new(
|
|
65
|
+
ctx: @ctx, caps: @write_caps,
|
|
66
|
+
rpc: rpc, writer: envelope_writer, hook_context: hook_context
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generated dispatch methods. Defined AFTER all use-cases have registered
|
|
71
|
+
# (Zeitwerk.eager_load runs in lib/textus.rb, then session.rb is explicitly
|
|
72
|
+
# required so UseCase.entries is fully populated).
|
|
73
|
+
Application::UseCase.each do |entry|
|
|
74
|
+
verb = entry.verb
|
|
75
|
+
mod = entry.mod
|
|
76
|
+
caps_sym = entry.caps_kind
|
|
77
|
+
|
|
78
|
+
define_method(verb) do |*args, **kwargs|
|
|
79
|
+
fixed = { session: self, ctx: @ctx, caps: caps_sym == :read ? @read_caps : @write_caps }
|
|
80
|
+
mod.call(*args, **fixed, **kwargs)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/textus/store.rb
CHANGED
|
@@ -2,7 +2,7 @@ require "fileutils"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
class Store
|
|
5
|
-
attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :
|
|
5
|
+
attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :events, :rpc
|
|
6
6
|
|
|
7
7
|
def self.discover(start_dir = Dir.pwd, root: nil)
|
|
8
8
|
explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
|
|
@@ -35,15 +35,20 @@ module Textus
|
|
|
35
35
|
@file_store = Infra::Storage::FileStore.new
|
|
36
36
|
@audit_log = Infra::AuditLog.new(
|
|
37
37
|
@root,
|
|
38
|
-
max_size: @manifest.audit_config[:max_size],
|
|
39
|
-
keep: @manifest.audit_config[:keep],
|
|
38
|
+
max_size: @manifest.data.audit_config[:max_size],
|
|
39
|
+
keep: @manifest.data.audit_config[:keep],
|
|
40
40
|
)
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Hooks::
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
@events = Hooks::EventBus.new
|
|
42
|
+
@rpc = Hooks::RpcRegistry.new
|
|
43
|
+
Infra::AuditSubscriber.new(@audit_log).attach(@events)
|
|
44
|
+
Hooks::Builtin.register_all(events: @events, rpc: @rpc)
|
|
45
|
+
Hooks::Loader.new(events: @events, rpc: @rpc).load_dir(File.join(@root, "hooks"))
|
|
46
|
+
sess = Session.for(self, role: Role::DEFAULT)
|
|
47
|
+
@events.publish(:store_loaded, ctx: sess.hook_context)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def session(role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
51
|
+
Session.for(self, role: role, correlation_id: correlation_id, dry_run: dry_run)
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
end
|
data/lib/textus/version.rb
CHANGED
data/lib/textus.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "zeitwerk"
|
|
2
2
|
require_relative "textus/version"
|
|
3
3
|
require_relative "textus/errors"
|
|
4
|
+
require_relative "textus/mcp"
|
|
5
|
+
require_relative "textus/mcp/errors"
|
|
4
6
|
|
|
5
7
|
loader = Zeitwerk::Loader.for_gem
|
|
6
8
|
loader.inflector.inflect(
|
|
@@ -8,11 +10,16 @@ loader.inflector.inflect(
|
|
|
8
10
|
"json" => "Json",
|
|
9
11
|
"yaml" => "Yaml",
|
|
10
12
|
"hook_dsl_scanner" => "HookDSLScanner",
|
|
11
|
-
"
|
|
13
|
+
"mcp" => "MCP",
|
|
14
|
+
"mcp_serve" => "MCPServe",
|
|
12
15
|
)
|
|
13
16
|
loader.ignore(File.expand_path("textus/errors.rb", __dir__))
|
|
17
|
+
loader.ignore(File.expand_path("textus/mcp.rb", __dir__))
|
|
18
|
+
loader.ignore(File.expand_path("textus/mcp/errors.rb", __dir__))
|
|
19
|
+
loader.ignore(File.expand_path("textus/session.rb", __dir__))
|
|
14
20
|
loader.setup
|
|
15
21
|
loader.eager_load
|
|
22
|
+
require_relative "textus/session"
|
|
16
23
|
|
|
17
24
|
module Textus
|
|
18
25
|
@hook_mutex = Mutex.new
|
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.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -109,40 +109,46 @@ files:
|
|
|
109
109
|
- docs/conventions.md
|
|
110
110
|
- exe/textus
|
|
111
111
|
- lib/textus.rb
|
|
112
|
+
- lib/textus/application/caps.rb
|
|
112
113
|
- lib/textus/application/context.rb
|
|
113
|
-
- lib/textus/application/
|
|
114
|
-
- lib/textus/application/
|
|
115
|
-
- lib/textus/application/
|
|
114
|
+
- lib/textus/application/envelope/reader.rb
|
|
115
|
+
- lib/textus/application/envelope/writer.rb
|
|
116
|
+
- lib/textus/application/maintenance.rb
|
|
117
|
+
- lib/textus/application/maintenance/key_delete_prefix.rb
|
|
118
|
+
- lib/textus/application/maintenance/key_mv_prefix.rb
|
|
119
|
+
- lib/textus/application/maintenance/migrate.rb
|
|
120
|
+
- lib/textus/application/maintenance/rule_lint.rb
|
|
121
|
+
- lib/textus/application/maintenance/zone_mv.rb
|
|
116
122
|
- lib/textus/application/projection.rb
|
|
117
|
-
- lib/textus/application/
|
|
118
|
-
- lib/textus/application/
|
|
119
|
-
- lib/textus/application/
|
|
120
|
-
- lib/textus/application/
|
|
121
|
-
- lib/textus/application/
|
|
122
|
-
- lib/textus/application/
|
|
123
|
-
- lib/textus/application/
|
|
124
|
-
- lib/textus/application/
|
|
125
|
-
- lib/textus/application/
|
|
126
|
-
- lib/textus/application/
|
|
127
|
-
- lib/textus/application/
|
|
128
|
-
- lib/textus/application/
|
|
129
|
-
- lib/textus/application/
|
|
130
|
-
- lib/textus/application/
|
|
131
|
-
- lib/textus/application/
|
|
132
|
-
- lib/textus/application/
|
|
133
|
-
- lib/textus/application/
|
|
134
|
-
- lib/textus/application/
|
|
135
|
-
- lib/textus/application/
|
|
136
|
-
- lib/textus/application/
|
|
137
|
-
- lib/textus/application/
|
|
138
|
-
- lib/textus/application/
|
|
139
|
-
- lib/textus/application/
|
|
140
|
-
- lib/textus/application/
|
|
141
|
-
- lib/textus/application/
|
|
142
|
-
- lib/textus/application/
|
|
143
|
-
- lib/textus/application/
|
|
144
|
-
- lib/textus/application/
|
|
145
|
-
- lib/textus/application/
|
|
123
|
+
- lib/textus/application/read/audit.rb
|
|
124
|
+
- lib/textus/application/read/blame.rb
|
|
125
|
+
- lib/textus/application/read/deps.rb
|
|
126
|
+
- lib/textus/application/read/freshness.rb
|
|
127
|
+
- lib/textus/application/read/get.rb
|
|
128
|
+
- lib/textus/application/read/get_or_refresh.rb
|
|
129
|
+
- lib/textus/application/read/list.rb
|
|
130
|
+
- lib/textus/application/read/policy_explain.rb
|
|
131
|
+
- lib/textus/application/read/published.rb
|
|
132
|
+
- lib/textus/application/read/pulse.rb
|
|
133
|
+
- lib/textus/application/read/rdeps.rb
|
|
134
|
+
- lib/textus/application/read/schema_envelope.rb
|
|
135
|
+
- lib/textus/application/read/stale.rb
|
|
136
|
+
- lib/textus/application/read/uid.rb
|
|
137
|
+
- lib/textus/application/read/validate_all.rb
|
|
138
|
+
- lib/textus/application/read/validator.rb
|
|
139
|
+
- lib/textus/application/read/where.rb
|
|
140
|
+
- lib/textus/application/use_case.rb
|
|
141
|
+
- lib/textus/application/write/accept.rb
|
|
142
|
+
- lib/textus/application/write/authority_gate.rb
|
|
143
|
+
- lib/textus/application/write/delete.rb
|
|
144
|
+
- lib/textus/application/write/materializer.rb
|
|
145
|
+
- lib/textus/application/write/mv.rb
|
|
146
|
+
- lib/textus/application/write/publish.rb
|
|
147
|
+
- lib/textus/application/write/put.rb
|
|
148
|
+
- lib/textus/application/write/refresh_all.rb
|
|
149
|
+
- lib/textus/application/write/refresh_orchestrator.rb
|
|
150
|
+
- lib/textus/application/write/refresh_worker.rb
|
|
151
|
+
- lib/textus/application/write/reject.rb
|
|
146
152
|
- lib/textus/boot.rb
|
|
147
153
|
- lib/textus/builder/pipeline.rb
|
|
148
154
|
- lib/textus/builder/renderer.rb
|
|
@@ -154,9 +160,11 @@ files:
|
|
|
154
160
|
- lib/textus/cli/group.rb
|
|
155
161
|
- lib/textus/cli/group/hook.rb
|
|
156
162
|
- lib/textus/cli/group/key.rb
|
|
163
|
+
- lib/textus/cli/group/mcp.rb
|
|
157
164
|
- lib/textus/cli/group/refresh.rb
|
|
158
165
|
- lib/textus/cli/group/rule.rb
|
|
159
166
|
- lib/textus/cli/group/schema.rb
|
|
167
|
+
- lib/textus/cli/group/zone.rb
|
|
160
168
|
- lib/textus/cli/verb.rb
|
|
161
169
|
- lib/textus/cli/verb/accept.rb
|
|
162
170
|
- lib/textus/cli/verb/audit.rb
|
|
@@ -171,7 +179,10 @@ files:
|
|
|
171
179
|
- lib/textus/cli/verb/hook_run.rb
|
|
172
180
|
- lib/textus/cli/verb/hooks.rb
|
|
173
181
|
- lib/textus/cli/verb/init.rb
|
|
182
|
+
- lib/textus/cli/verb/key_delete.rb
|
|
174
183
|
- lib/textus/cli/verb/list.rb
|
|
184
|
+
- lib/textus/cli/verb/mcp_serve.rb
|
|
185
|
+
- lib/textus/cli/verb/migrate.rb
|
|
175
186
|
- lib/textus/cli/verb/mv.rb
|
|
176
187
|
- lib/textus/cli/verb/published.rb
|
|
177
188
|
- lib/textus/cli/verb/pulse.rb
|
|
@@ -181,6 +192,7 @@ files:
|
|
|
181
192
|
- lib/textus/cli/verb/refresh_stale.rb
|
|
182
193
|
- lib/textus/cli/verb/reject.rb
|
|
183
194
|
- lib/textus/cli/verb/rule_explain.rb
|
|
195
|
+
- lib/textus/cli/verb/rule_lint.rb
|
|
184
196
|
- lib/textus/cli/verb/rule_list.rb
|
|
185
197
|
- lib/textus/cli/verb/schema.rb
|
|
186
198
|
- lib/textus/cli/verb/schema_diff.rb
|
|
@@ -188,6 +200,7 @@ files:
|
|
|
188
200
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
189
201
|
- lib/textus/cli/verb/uid.rb
|
|
190
202
|
- lib/textus/cli/verb/where.rb
|
|
203
|
+
- lib/textus/cli/verb/zone_mv.rb
|
|
191
204
|
- lib/textus/doctor.rb
|
|
192
205
|
- lib/textus/doctor/check.rb
|
|
193
206
|
- lib/textus/doctor/check/audit_log.rb
|
|
@@ -215,7 +228,10 @@ files:
|
|
|
215
228
|
- lib/textus/domain/permission.rb
|
|
216
229
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
217
230
|
- lib/textus/domain/policy/matcher.rb
|
|
231
|
+
- lib/textus/domain/policy/predicates/accept_authority_signed.rb
|
|
232
|
+
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
218
233
|
- lib/textus/domain/policy/promote.rb
|
|
234
|
+
- lib/textus/domain/policy/promotion.rb
|
|
219
235
|
- lib/textus/domain/policy/refresh.rb
|
|
220
236
|
- lib/textus/domain/sentinel.rb
|
|
221
237
|
- lib/textus/domain/staleness.rb
|
|
@@ -231,15 +247,16 @@ files:
|
|
|
231
247
|
- lib/textus/errors.rb
|
|
232
248
|
- lib/textus/etag.rb
|
|
233
249
|
- lib/textus/hooks/builtin.rb
|
|
234
|
-
- lib/textus/hooks/bus.rb
|
|
235
250
|
- lib/textus/hooks/context.rb
|
|
251
|
+
- lib/textus/hooks/error_log.rb
|
|
252
|
+
- lib/textus/hooks/event_bus.rb
|
|
236
253
|
- lib/textus/hooks/fire_report.rb
|
|
237
254
|
- lib/textus/hooks/loader.rb
|
|
255
|
+
- lib/textus/hooks/rpc_registry.rb
|
|
238
256
|
- lib/textus/infra/audit_log.rb
|
|
239
257
|
- lib/textus/infra/audit_subscriber.rb
|
|
240
258
|
- lib/textus/infra/build_lock.rb
|
|
241
259
|
- lib/textus/infra/clock.rb
|
|
242
|
-
- lib/textus/infra/event_bus.rb
|
|
243
260
|
- lib/textus/infra/publisher.rb
|
|
244
261
|
- lib/textus/infra/refresh/detached.rb
|
|
245
262
|
- lib/textus/infra/refresh/lock.rb
|
|
@@ -249,6 +266,7 @@ files:
|
|
|
249
266
|
- lib/textus/key/grammar.rb
|
|
250
267
|
- lib/textus/key/path.rb
|
|
251
268
|
- lib/textus/manifest.rb
|
|
269
|
+
- lib/textus/manifest/data.rb
|
|
252
270
|
- lib/textus/manifest/entry.rb
|
|
253
271
|
- lib/textus/manifest/entry/base.rb
|
|
254
272
|
- lib/textus/manifest/entry/derived.rb
|
|
@@ -262,16 +280,23 @@ files:
|
|
|
262
280
|
- lib/textus/manifest/entry/validators/index_filename.rb
|
|
263
281
|
- lib/textus/manifest/entry/validators/inject_boot.rb
|
|
264
282
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
283
|
+
- lib/textus/manifest/policy.rb
|
|
265
284
|
- lib/textus/manifest/resolver.rb
|
|
266
285
|
- lib/textus/manifest/role_kinds.rb
|
|
267
286
|
- lib/textus/manifest/rules.rb
|
|
268
287
|
- lib/textus/manifest/schema.rb
|
|
288
|
+
- lib/textus/mcp.rb
|
|
289
|
+
- lib/textus/mcp/errors.rb
|
|
290
|
+
- lib/textus/mcp/server.rb
|
|
291
|
+
- lib/textus/mcp/session.rb
|
|
292
|
+
- lib/textus/mcp/tool_schemas.rb
|
|
293
|
+
- lib/textus/mcp/tools.rb
|
|
269
294
|
- lib/textus/mustache.rb
|
|
270
|
-
- lib/textus/operations.rb
|
|
271
295
|
- lib/textus/role.rb
|
|
272
296
|
- lib/textus/schema.rb
|
|
273
297
|
- lib/textus/schema/tools.rb
|
|
274
298
|
- lib/textus/schemas.rb
|
|
299
|
+
- lib/textus/session.rb
|
|
275
300
|
- lib/textus/store.rb
|
|
276
301
|
- lib/textus/uid.rb
|
|
277
302
|
- lib/textus/version.rb
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "time"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Application
|
|
6
|
-
module Reads
|
|
7
|
-
# Queries .textus/audit.log. Filters: key, zone, role, verb, since,
|
|
8
|
-
# correlation_id, limit. Reads the log file as JSON-Lines (legacy TSV
|
|
9
|
-
# rows produce nil and are skipped).
|
|
10
|
-
class Audit
|
|
11
|
-
def initialize(manifest:, root:, audit_log: nil)
|
|
12
|
-
@manifest = manifest
|
|
13
|
-
@root = root
|
|
14
|
-
@log_path = File.join(root, "audit.log")
|
|
15
|
-
@audit_log = audit_log
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
19
|
-
def call(key: nil, zone: nil, role: nil, verb: nil, since: nil, seq_since: nil, correlation_id: nil, limit: nil)
|
|
20
|
-
check_cursor_expiry!(seq_since)
|
|
21
|
-
|
|
22
|
-
files = all_log_files
|
|
23
|
-
return [] if files.empty?
|
|
24
|
-
|
|
25
|
-
rows = []
|
|
26
|
-
files.each do |file|
|
|
27
|
-
File.foreach(file) do |line|
|
|
28
|
-
parsed = parse_row(line.chomp)
|
|
29
|
-
next unless parsed
|
|
30
|
-
next if key && parsed["key"] != key
|
|
31
|
-
next if role && parsed["role"] != role
|
|
32
|
-
next if verb && parsed["verb"] != verb
|
|
33
|
-
next if zone && !key_in_zone?(parsed["key"], zone)
|
|
34
|
-
next if since && (parsed["ts"].nil? || Time.parse(parsed["ts"]) < since)
|
|
35
|
-
next if seq_since && (parsed["seq"].nil? || parsed["seq"] <= seq_since)
|
|
36
|
-
next if correlation_id && parsed.dig("extras", "correlation_id") != correlation_id
|
|
37
|
-
|
|
38
|
-
rows << parsed
|
|
39
|
-
break if limit && rows.length >= limit
|
|
40
|
-
end
|
|
41
|
-
break if limit && rows.length >= limit
|
|
42
|
-
end
|
|
43
|
-
rows
|
|
44
|
-
end
|
|
45
|
-
# rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
46
|
-
|
|
47
|
-
# Accepts ISO8601 ("2026-01-15", "2026-01-15T10:00:00Z") or a relative
|
|
48
|
-
# offset matching /\A(\d+)([smhd])\z/. Returns nil for unparseable input.
|
|
49
|
-
def self.parse_since(str, now: Time.now.utc)
|
|
50
|
-
return nil if str.nil? || str.empty?
|
|
51
|
-
return Time.parse(str) if str =~ /\A\d{4}-\d{2}-\d{2}/
|
|
52
|
-
|
|
53
|
-
m = str.match(/\A(\d+)([smhd])\z/) or return nil
|
|
54
|
-
mult = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }[m[2]]
|
|
55
|
-
now - (m[1].to_i * mult)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def check_cursor_expiry!(seq_since)
|
|
61
|
-
return unless seq_since
|
|
62
|
-
|
|
63
|
-
log = @audit_log || Textus::Infra::AuditLog.new(@root)
|
|
64
|
-
min = log.min_available_seq
|
|
65
|
-
raise Textus::CursorExpired.new(requested: seq_since, min_available: min) if min && seq_since < min - 1
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def all_log_files
|
|
69
|
-
rotated = Dir.glob(File.join(@root, "audit.log.*"))
|
|
70
|
-
.reject { |p| p.end_with?(".meta.json") }
|
|
71
|
-
.sort_by { |p| -p.scan(/\d+$/).first.to_i } # .5 .4 .3 .2 .1 → oldest first
|
|
72
|
-
active = File.exist?(@log_path) ? [@log_path] : []
|
|
73
|
-
rotated + active
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def parse_row(line)
|
|
77
|
-
return nil if line.empty?
|
|
78
|
-
return nil unless line.start_with?("{")
|
|
79
|
-
|
|
80
|
-
JSON.parse(line)
|
|
81
|
-
rescue JSON::ParserError
|
|
82
|
-
nil
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def key_in_zone?(key, zone)
|
|
86
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
87
|
-
mentry && mentry.zone == zone
|
|
88
|
-
rescue Textus::Error
|
|
89
|
-
false
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|