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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Write
|
|
5
|
+
class RefreshWorker
|
|
6
|
+
FETCH_TIMEOUT_SECONDS = 30
|
|
7
|
+
|
|
8
|
+
def initialize(container:, call:)
|
|
9
|
+
@container = container
|
|
10
|
+
@call = call
|
|
11
|
+
@manifest = container.manifest
|
|
12
|
+
@events = container.events
|
|
13
|
+
@rpc = container.rpc
|
|
14
|
+
@authorizer = container.authorizer
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# call(key) is the primary entry; run is kept as an alias for
|
|
18
|
+
# Orchestrator and RefreshAll which call worker.run(key).
|
|
19
|
+
def call(key)
|
|
20
|
+
run(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(key)
|
|
24
|
+
res = @manifest.resolver.resolve(key)
|
|
25
|
+
mentry = res.entry
|
|
26
|
+
path = res.path
|
|
27
|
+
remaining = res.remaining
|
|
28
|
+
raise UsageError.new("no intake declared for '#{key}'") unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
29
|
+
|
|
30
|
+
before_etag = File.exist?(path) ? Etag.for_file(path) : nil
|
|
31
|
+
result = fetch_with_events(key, mentry, remaining)
|
|
32
|
+
persist_and_notify(key, mentry, result, before_etag)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.normalize_action_result(res, format:)
|
|
36
|
+
res = res.transform_keys(&:to_s) if res.is_a?(Hash)
|
|
37
|
+
res ||= {}
|
|
38
|
+
meta_val = res["_meta"]
|
|
39
|
+
body = res["body"]
|
|
40
|
+
content = res["content"]
|
|
41
|
+
|
|
42
|
+
case format
|
|
43
|
+
when "markdown" then { meta: meta_val || {}, body: body.to_s, content: nil }
|
|
44
|
+
when "text" then { meta: {}, body: body.to_s, content: nil }
|
|
45
|
+
when "json", "yaml"
|
|
46
|
+
if !content.nil?
|
|
47
|
+
{ meta: meta_val || {}, body: nil, content: content }
|
|
48
|
+
elsif !body.nil?
|
|
49
|
+
{ meta: {}, body: body.to_s, content: nil }
|
|
50
|
+
else
|
|
51
|
+
raise Textus::UsageError.new("intake for #{format} returned neither content nor body")
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
raise Textus::UsageError.new("unknown format #{format.inspect}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def hook_context
|
|
61
|
+
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_timeout_for(key)
|
|
65
|
+
rule = @manifest.rules.for(key)
|
|
66
|
+
rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fetch_with_events(key, mentry, remaining)
|
|
70
|
+
@events.publish(:refresh_started, ctx: hook_context, key: key, mode: :sync)
|
|
71
|
+
call_intake(key, mentry, remaining)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def call_intake(key, mentry, remaining)
|
|
75
|
+
timeout = fetch_timeout_for(key)
|
|
76
|
+
Timeout.timeout(timeout) do
|
|
77
|
+
@rpc.invoke(:resolve_intake, mentry.handler,
|
|
78
|
+
caps: @container,
|
|
79
|
+
config: mentry.config,
|
|
80
|
+
args: { trigger_key: key, leaf_segments: remaining || [] })
|
|
81
|
+
end
|
|
82
|
+
rescue Timeout::Error
|
|
83
|
+
@events.publish(:refresh_failed, ctx: hook_context, key: key,
|
|
84
|
+
error_class: "Timeout::Error",
|
|
85
|
+
error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
|
|
86
|
+
raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
|
|
87
|
+
rescue Textus::Error => e
|
|
88
|
+
@events.publish(:refresh_failed, ctx: hook_context, key: key, error_class: e.class.name,
|
|
89
|
+
error_message: e.message)
|
|
90
|
+
raise
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
@events.publish(:refresh_failed, ctx: hook_context, key: key, error_class: e.class.name,
|
|
93
|
+
error_message: e.message)
|
|
94
|
+
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def persist_and_notify(key, mentry, result, before_etag)
|
|
98
|
+
normalized = self.class.normalize_action_result(result, format: mentry.format)
|
|
99
|
+
@authorizer.authorize_write!(mentry, role: @call.role)
|
|
100
|
+
envelope = writer.put(
|
|
101
|
+
key,
|
|
102
|
+
mentry: mentry,
|
|
103
|
+
payload: Textus::Envelope::IO::Writer::Payload.new(
|
|
104
|
+
meta: normalized[:meta], body: normalized[:body], content: normalized[:content],
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
change = detect_change(before_etag, envelope)
|
|
108
|
+
@events.publish(:entry_refreshed, ctx: hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
|
|
109
|
+
envelope
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def detect_change(before_etag, envelope)
|
|
113
|
+
if before_etag.nil? then :created
|
|
114
|
+
elsif envelope.etag == before_etag then :unchanged
|
|
115
|
+
else :updated
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def writer
|
|
120
|
+
@writer ||= Textus::Envelope::IO::Writer.new(
|
|
121
|
+
file_store: @container.file_store,
|
|
122
|
+
manifest: @container.manifest,
|
|
123
|
+
schemas: @container.schemas,
|
|
124
|
+
audit_log: @container.audit_log,
|
|
125
|
+
call: @call,
|
|
126
|
+
reader: reader,
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reader
|
|
131
|
+
@reader ||= Textus::Envelope::IO::Reader.new(
|
|
132
|
+
file_store: @container.file_store,
|
|
133
|
+
manifest: @container.manifest,
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative "authority_gate"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Write
|
|
5
|
+
class Reject
|
|
6
|
+
include AuthorityGate
|
|
7
|
+
|
|
8
|
+
def initialize(container:, call:)
|
|
9
|
+
@container = container
|
|
10
|
+
@call = call
|
|
11
|
+
@manifest = container.manifest
|
|
12
|
+
@events = container.events
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(pending_key)
|
|
16
|
+
assert_accept_authority!("reject")
|
|
17
|
+
|
|
18
|
+
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
19
|
+
unless mentry.in_proposal_zone?(@manifest.policy)
|
|
20
|
+
raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
env = Textus::Read::Get.new(
|
|
24
|
+
container: @container, call: @call,
|
|
25
|
+
).call(pending_key)
|
|
26
|
+
proposal = env.meta&.dig("proposal") or
|
|
27
|
+
raise ProposalError.new("entry has no proposal block: #{pending_key}")
|
|
28
|
+
target_key = proposal["target_key"] or
|
|
29
|
+
raise ProposalError.new("proposal missing target_key")
|
|
30
|
+
|
|
31
|
+
delete_op.call(pending_key, suppress_events: true)
|
|
32
|
+
|
|
33
|
+
@events.publish(:proposal_rejected,
|
|
34
|
+
ctx: hook_context,
|
|
35
|
+
key: pending_key,
|
|
36
|
+
target_key: target_key)
|
|
37
|
+
|
|
38
|
+
{ "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def hook_context
|
|
44
|
+
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete_op
|
|
48
|
+
@delete_op ||= Textus::Write::Delete.new(
|
|
49
|
+
container: @container, call: @call,
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
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,9 +10,13 @@ loader.inflector.inflect(
|
|
|
8
10
|
"json" => "Json",
|
|
9
11
|
"yaml" => "Yaml",
|
|
10
12
|
"hook_dsl_scanner" => "HookDSLScanner",
|
|
11
|
-
"
|
|
13
|
+
"io" => "IO",
|
|
14
|
+
"mcp" => "MCP",
|
|
15
|
+
"mcp_serve" => "MCPServe",
|
|
12
16
|
)
|
|
13
17
|
loader.ignore(File.expand_path("textus/errors.rb", __dir__))
|
|
18
|
+
loader.ignore(File.expand_path("textus/mcp.rb", __dir__))
|
|
19
|
+
loader.ignore(File.expand_path("textus/mcp/errors.rb", __dir__))
|
|
14
20
|
loader.setup
|
|
15
21
|
loader.eager_load
|
|
16
22
|
|
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.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -109,40 +109,6 @@ files:
|
|
|
109
109
|
- docs/conventions.md
|
|
110
110
|
- exe/textus
|
|
111
111
|
- lib/textus.rb
|
|
112
|
-
- lib/textus/application/context.rb
|
|
113
|
-
- lib/textus/application/policy/predicates/accept_authority_signed.rb
|
|
114
|
-
- lib/textus/application/policy/predicates/schema_valid.rb
|
|
115
|
-
- lib/textus/application/policy/promotion.rb
|
|
116
|
-
- lib/textus/application/projection.rb
|
|
117
|
-
- lib/textus/application/reads/audit.rb
|
|
118
|
-
- lib/textus/application/reads/blame.rb
|
|
119
|
-
- lib/textus/application/reads/deps.rb
|
|
120
|
-
- lib/textus/application/reads/freshness.rb
|
|
121
|
-
- lib/textus/application/reads/get.rb
|
|
122
|
-
- lib/textus/application/reads/get_or_refresh.rb
|
|
123
|
-
- lib/textus/application/reads/list.rb
|
|
124
|
-
- lib/textus/application/reads/policy_explain.rb
|
|
125
|
-
- lib/textus/application/reads/published.rb
|
|
126
|
-
- lib/textus/application/reads/pulse.rb
|
|
127
|
-
- lib/textus/application/reads/rdeps.rb
|
|
128
|
-
- lib/textus/application/reads/schema_envelope.rb
|
|
129
|
-
- lib/textus/application/reads/stale.rb
|
|
130
|
-
- lib/textus/application/reads/uid.rb
|
|
131
|
-
- lib/textus/application/reads/validate_all.rb
|
|
132
|
-
- lib/textus/application/reads/validator.rb
|
|
133
|
-
- lib/textus/application/reads/where.rb
|
|
134
|
-
- lib/textus/application/refresh/all.rb
|
|
135
|
-
- lib/textus/application/refresh/orchestrator.rb
|
|
136
|
-
- lib/textus/application/refresh/worker.rb
|
|
137
|
-
- lib/textus/application/writes/accept.rb
|
|
138
|
-
- lib/textus/application/writes/authority_gate.rb
|
|
139
|
-
- lib/textus/application/writes/delete.rb
|
|
140
|
-
- lib/textus/application/writes/envelope_io.rb
|
|
141
|
-
- lib/textus/application/writes/materializer.rb
|
|
142
|
-
- lib/textus/application/writes/mv.rb
|
|
143
|
-
- lib/textus/application/writes/publish.rb
|
|
144
|
-
- lib/textus/application/writes/put.rb
|
|
145
|
-
- lib/textus/application/writes/reject.rb
|
|
146
112
|
- lib/textus/boot.rb
|
|
147
113
|
- lib/textus/builder/pipeline.rb
|
|
148
114
|
- lib/textus/builder/renderer.rb
|
|
@@ -150,13 +116,16 @@ files:
|
|
|
150
116
|
- lib/textus/builder/renderer/markdown.rb
|
|
151
117
|
- lib/textus/builder/renderer/text.rb
|
|
152
118
|
- lib/textus/builder/renderer/yaml.rb
|
|
119
|
+
- lib/textus/call.rb
|
|
153
120
|
- lib/textus/cli.rb
|
|
154
121
|
- lib/textus/cli/group.rb
|
|
155
122
|
- lib/textus/cli/group/hook.rb
|
|
156
123
|
- lib/textus/cli/group/key.rb
|
|
124
|
+
- lib/textus/cli/group/mcp.rb
|
|
157
125
|
- lib/textus/cli/group/refresh.rb
|
|
158
126
|
- lib/textus/cli/group/rule.rb
|
|
159
127
|
- lib/textus/cli/group/schema.rb
|
|
128
|
+
- lib/textus/cli/group/zone.rb
|
|
160
129
|
- lib/textus/cli/verb.rb
|
|
161
130
|
- lib/textus/cli/verb/accept.rb
|
|
162
131
|
- lib/textus/cli/verb/audit.rb
|
|
@@ -171,7 +140,10 @@ files:
|
|
|
171
140
|
- lib/textus/cli/verb/hook_run.rb
|
|
172
141
|
- lib/textus/cli/verb/hooks.rb
|
|
173
142
|
- lib/textus/cli/verb/init.rb
|
|
143
|
+
- lib/textus/cli/verb/key_delete.rb
|
|
174
144
|
- lib/textus/cli/verb/list.rb
|
|
145
|
+
- lib/textus/cli/verb/mcp_serve.rb
|
|
146
|
+
- lib/textus/cli/verb/migrate.rb
|
|
175
147
|
- lib/textus/cli/verb/mv.rb
|
|
176
148
|
- lib/textus/cli/verb/published.rb
|
|
177
149
|
- lib/textus/cli/verb/pulse.rb
|
|
@@ -181,6 +153,7 @@ files:
|
|
|
181
153
|
- lib/textus/cli/verb/refresh_stale.rb
|
|
182
154
|
- lib/textus/cli/verb/reject.rb
|
|
183
155
|
- lib/textus/cli/verb/rule_explain.rb
|
|
156
|
+
- lib/textus/cli/verb/rule_lint.rb
|
|
184
157
|
- lib/textus/cli/verb/rule_list.rb
|
|
185
158
|
- lib/textus/cli/verb/schema.rb
|
|
186
159
|
- lib/textus/cli/verb/schema_diff.rb
|
|
@@ -188,6 +161,9 @@ files:
|
|
|
188
161
|
- lib/textus/cli/verb/schema_migrate.rb
|
|
189
162
|
- lib/textus/cli/verb/uid.rb
|
|
190
163
|
- lib/textus/cli/verb/where.rb
|
|
164
|
+
- lib/textus/cli/verb/zone_mv.rb
|
|
165
|
+
- lib/textus/container.rb
|
|
166
|
+
- lib/textus/dispatcher.rb
|
|
191
167
|
- lib/textus/doctor.rb
|
|
192
168
|
- lib/textus/doctor/check.rb
|
|
193
169
|
- lib/textus/doctor/check/audit_log.rb
|
|
@@ -215,7 +191,10 @@ files:
|
|
|
215
191
|
- lib/textus/domain/permission.rb
|
|
216
192
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
217
193
|
- lib/textus/domain/policy/matcher.rb
|
|
194
|
+
- lib/textus/domain/policy/predicates/accept_authority_signed.rb
|
|
195
|
+
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
218
196
|
- lib/textus/domain/policy/promote.rb
|
|
197
|
+
- lib/textus/domain/policy/promotion.rb
|
|
219
198
|
- lib/textus/domain/policy/refresh.rb
|
|
220
199
|
- lib/textus/domain/sentinel.rb
|
|
221
200
|
- lib/textus/domain/staleness.rb
|
|
@@ -228,27 +207,29 @@ files:
|
|
|
228
207
|
- lib/textus/entry/text.rb
|
|
229
208
|
- lib/textus/entry/yaml.rb
|
|
230
209
|
- lib/textus/envelope.rb
|
|
210
|
+
- lib/textus/envelope/io/reader.rb
|
|
211
|
+
- lib/textus/envelope/io/writer.rb
|
|
231
212
|
- lib/textus/errors.rb
|
|
232
213
|
- lib/textus/etag.rb
|
|
233
214
|
- lib/textus/hooks/builtin.rb
|
|
234
|
-
- lib/textus/hooks/bus.rb
|
|
235
215
|
- lib/textus/hooks/context.rb
|
|
216
|
+
- lib/textus/hooks/error_log.rb
|
|
217
|
+
- lib/textus/hooks/event_bus.rb
|
|
236
218
|
- lib/textus/hooks/fire_report.rb
|
|
237
219
|
- lib/textus/hooks/loader.rb
|
|
238
|
-
- lib/textus/
|
|
239
|
-
- lib/textus/infra/audit_subscriber.rb
|
|
240
|
-
- lib/textus/infra/build_lock.rb
|
|
241
|
-
- lib/textus/infra/clock.rb
|
|
242
|
-
- lib/textus/infra/event_bus.rb
|
|
243
|
-
- lib/textus/infra/publisher.rb
|
|
244
|
-
- lib/textus/infra/refresh/detached.rb
|
|
245
|
-
- lib/textus/infra/refresh/lock.rb
|
|
246
|
-
- lib/textus/infra/storage/file_store.rb
|
|
220
|
+
- lib/textus/hooks/rpc_registry.rb
|
|
247
221
|
- lib/textus/init.rb
|
|
248
222
|
- lib/textus/key/distance.rb
|
|
249
223
|
- lib/textus/key/grammar.rb
|
|
250
224
|
- lib/textus/key/path.rb
|
|
225
|
+
- lib/textus/maintenance.rb
|
|
226
|
+
- lib/textus/maintenance/key_delete_prefix.rb
|
|
227
|
+
- lib/textus/maintenance/key_mv_prefix.rb
|
|
228
|
+
- lib/textus/maintenance/migrate.rb
|
|
229
|
+
- lib/textus/maintenance/rule_lint.rb
|
|
230
|
+
- lib/textus/maintenance/zone_mv.rb
|
|
251
231
|
- lib/textus/manifest.rb
|
|
232
|
+
- lib/textus/manifest/data.rb
|
|
252
233
|
- lib/textus/manifest/entry.rb
|
|
253
234
|
- lib/textus/manifest/entry/base.rb
|
|
254
235
|
- lib/textus/manifest/entry/derived.rb
|
|
@@ -262,19 +243,67 @@ files:
|
|
|
262
243
|
- lib/textus/manifest/entry/validators/index_filename.rb
|
|
263
244
|
- lib/textus/manifest/entry/validators/inject_boot.rb
|
|
264
245
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
246
|
+
- lib/textus/manifest/policy.rb
|
|
265
247
|
- lib/textus/manifest/resolver.rb
|
|
266
248
|
- lib/textus/manifest/role_kinds.rb
|
|
267
249
|
- lib/textus/manifest/rules.rb
|
|
268
250
|
- lib/textus/manifest/schema.rb
|
|
251
|
+
- lib/textus/mcp.rb
|
|
252
|
+
- lib/textus/mcp/errors.rb
|
|
253
|
+
- lib/textus/mcp/server.rb
|
|
254
|
+
- lib/textus/mcp/session.rb
|
|
255
|
+
- lib/textus/mcp/tool_schemas.rb
|
|
256
|
+
- lib/textus/mcp/tools.rb
|
|
269
257
|
- lib/textus/mustache.rb
|
|
270
|
-
- lib/textus/
|
|
258
|
+
- lib/textus/ports/audit_log.rb
|
|
259
|
+
- lib/textus/ports/audit_subscriber.rb
|
|
260
|
+
- lib/textus/ports/build_lock.rb
|
|
261
|
+
- lib/textus/ports/clock.rb
|
|
262
|
+
- lib/textus/ports/publisher.rb
|
|
263
|
+
- lib/textus/ports/refresh/detached.rb
|
|
264
|
+
- lib/textus/ports/refresh/lock.rb
|
|
265
|
+
- lib/textus/ports/sentinel_store.rb
|
|
266
|
+
- lib/textus/ports/storage/file_stat.rb
|
|
267
|
+
- lib/textus/ports/storage/file_store.rb
|
|
268
|
+
- lib/textus/projection.rb
|
|
269
|
+
- lib/textus/read/audit.rb
|
|
270
|
+
- lib/textus/read/blame.rb
|
|
271
|
+
- lib/textus/read/boot.rb
|
|
272
|
+
- lib/textus/read/deps.rb
|
|
273
|
+
- lib/textus/read/doctor.rb
|
|
274
|
+
- lib/textus/read/freshness.rb
|
|
275
|
+
- lib/textus/read/get.rb
|
|
276
|
+
- lib/textus/read/get_or_refresh.rb
|
|
277
|
+
- lib/textus/read/list.rb
|
|
278
|
+
- lib/textus/read/policy_explain.rb
|
|
279
|
+
- lib/textus/read/published.rb
|
|
280
|
+
- lib/textus/read/pulse.rb
|
|
281
|
+
- lib/textus/read/rdeps.rb
|
|
282
|
+
- lib/textus/read/schema_envelope.rb
|
|
283
|
+
- lib/textus/read/stale.rb
|
|
284
|
+
- lib/textus/read/uid.rb
|
|
285
|
+
- lib/textus/read/validate_all.rb
|
|
286
|
+
- lib/textus/read/validator.rb
|
|
287
|
+
- lib/textus/read/where.rb
|
|
271
288
|
- lib/textus/role.rb
|
|
289
|
+
- lib/textus/role_scope.rb
|
|
272
290
|
- lib/textus/schema.rb
|
|
273
291
|
- lib/textus/schema/tools.rb
|
|
274
292
|
- lib/textus/schemas.rb
|
|
275
293
|
- lib/textus/store.rb
|
|
276
294
|
- lib/textus/uid.rb
|
|
277
295
|
- lib/textus/version.rb
|
|
296
|
+
- lib/textus/write/accept.rb
|
|
297
|
+
- lib/textus/write/authority_gate.rb
|
|
298
|
+
- lib/textus/write/delete.rb
|
|
299
|
+
- lib/textus/write/materializer.rb
|
|
300
|
+
- lib/textus/write/mv.rb
|
|
301
|
+
- lib/textus/write/publish.rb
|
|
302
|
+
- lib/textus/write/put.rb
|
|
303
|
+
- lib/textus/write/refresh_all.rb
|
|
304
|
+
- lib/textus/write/refresh_orchestrator.rb
|
|
305
|
+
- lib/textus/write/refresh_worker.rb
|
|
306
|
+
- lib/textus/write/reject.rb
|
|
278
307
|
homepage: https://github.com/patrick204nqh/textus
|
|
279
308
|
licenses:
|
|
280
309
|
- MIT
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
require "securerandom"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Application
|
|
5
|
-
# A Context describes the call: who is acting (role), what request this
|
|
6
|
-
# is part of (correlation_id), what time it is (now), and whether
|
|
7
|
-
# writes should be suppressed (dry_run).
|
|
8
|
-
#
|
|
9
|
-
# Collaborators (manifest, file_store, bus, audit log, authorizer) are
|
|
10
|
-
# never read from Context — use cases declare them as explicit
|
|
11
|
-
# constructor ports, and Operations wires them in from the Store.
|
|
12
|
-
Context = Data.define(:role, :correlation_id, :now, :dry_run) do
|
|
13
|
-
def self.build(role:, correlation_id: nil, now: nil, dry_run: false)
|
|
14
|
-
new(
|
|
15
|
-
role: role.to_s,
|
|
16
|
-
correlation_id: correlation_id || SecureRandom.uuid,
|
|
17
|
-
now: now || Time.now,
|
|
18
|
-
dry_run: dry_run,
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def dry_run? = dry_run
|
|
23
|
-
|
|
24
|
-
def with_role(new_role)
|
|
25
|
-
self.class.new(
|
|
26
|
-
role: new_role.to_s,
|
|
27
|
-
correlation_id: correlation_id,
|
|
28
|
-
now: now,
|
|
29
|
-
dry_run: dry_run,
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
require "timeout"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Application
|
|
6
|
-
class Projection
|
|
7
|
-
MAX_LIMIT = 1000
|
|
8
|
-
REDUCER_TIMEOUT_SECONDS = 2
|
|
9
|
-
|
|
10
|
-
# `reader` — a callable `->(key) { envelope_or_nil }`. Caller picks
|
|
11
|
-
# semantics: pure read (`ops.get`) for materialization paths;
|
|
12
|
-
# `ops.get_or_refresh` if you want refresh-on-stale.
|
|
13
|
-
# `lister` — a callable `->(prefix:) { [ { "key" => ... }, ... ] }`.
|
|
14
|
-
# `transform_resolver` — a callable `->(name) { callable_or_raise }`.
|
|
15
|
-
# `transform_context` — `Application::Context` handed to the transform reducer.
|
|
16
|
-
def initialize(reader:, spec:, lister:, transform_resolver:, transform_context:)
|
|
17
|
-
@reader = reader
|
|
18
|
-
@spec = spec || {}
|
|
19
|
-
@lister = lister
|
|
20
|
-
@transform_resolver = transform_resolver
|
|
21
|
-
@transform_context = transform_context
|
|
22
|
-
@limit = (@spec["limit"] || MAX_LIMIT).to_i
|
|
23
|
-
raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def run
|
|
27
|
-
keys = collect_keys
|
|
28
|
-
explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
|
|
29
|
-
rows = keys.map do |key|
|
|
30
|
-
env = @reader.call(key)
|
|
31
|
-
row = pluck(env.meta, env.body)
|
|
32
|
-
explicit_pluck ? row : row.merge("_key" => key)
|
|
33
|
-
end
|
|
34
|
-
reduced = apply_reducer(rows)
|
|
35
|
-
# Reducers may return either an Array of rows (legacy / templated builds)
|
|
36
|
-
# or a Hash that becomes the structured-format payload base. In the Hash
|
|
37
|
-
# case, downstream sort/limit/position markers don't apply, and the
|
|
38
|
-
# builder owns `_meta.generated_at` so we don't stamp it here.
|
|
39
|
-
return reduced if reduced.is_a?(Hash)
|
|
40
|
-
|
|
41
|
-
rows = reduced
|
|
42
|
-
rows = sort(rows)
|
|
43
|
-
rows = rows.first(@limit)
|
|
44
|
-
mark_positions(rows)
|
|
45
|
-
{ "entries" => rows, "count" => rows.length, "generated_at" => Time.now.utc.iso8601 }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def apply_reducer(rows)
|
|
51
|
-
name = @spec["transform"] or return rows
|
|
52
|
-
callable = @transform_resolver.call(name)
|
|
53
|
-
Timeout.timeout(REDUCER_TIMEOUT_SECONDS) do
|
|
54
|
-
callable.call(store: @transform_context, rows: rows, config: @spec["transform_config"] || {})
|
|
55
|
-
end
|
|
56
|
-
rescue Timeout::Error
|
|
57
|
-
raise UsageError.new("transform_rows '#{name}' exceeded #{REDUCER_TIMEOUT_SECONDS}s timeout")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def collect_keys
|
|
61
|
-
prefixes = Array(@spec["select"])
|
|
62
|
-
prefixes.flat_map { |p| @lister.call(prefix: p).map { |row| row["key"] } }.uniq
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def pluck(frontmatter, _body)
|
|
66
|
-
fields = @spec["pluck"]
|
|
67
|
-
if fields.nil? || fields == "*"
|
|
68
|
-
frontmatter
|
|
69
|
-
else
|
|
70
|
-
Array(fields).each_with_object({}) { |f, h| h[f] = frontmatter[f] if frontmatter.key?(f) }
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Adds `_first`, `_last`, and `_index` markers so templates can emit
|
|
75
|
-
# delimiters (e.g. JSON commas) via {{^_last}},{{/_last}}.
|
|
76
|
-
def mark_positions(rows)
|
|
77
|
-
last_idx = rows.length - 1
|
|
78
|
-
rows.each_with_index do |row, i|
|
|
79
|
-
row["_index"] = i
|
|
80
|
-
row["_first"] = i.zero?
|
|
81
|
-
row["_last"] = (i == last_idx)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def sort(rows)
|
|
86
|
-
sb = @spec["sort_by"] or return rows
|
|
87
|
-
rows.sort_by { |r| r[sb].to_s }
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -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
|