textus 0.55.1 → 0.55.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +9 -9
- data/SPEC.md +14 -13
- data/docs/architecture/README.md +3 -3
- data/docs/reference/conventions.md +5 -2
- data/lib/textus/boot.rb +64 -85
- data/lib/textus/{gate → dispatch}/binder.rb +8 -10
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +1 -1
- data/lib/textus/doctor/check.rb +8 -6
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +2 -0
- data/lib/textus/format/base.rb +36 -8
- data/lib/textus/format/json.rb +0 -21
- data/lib/textus/format/markdown.rb +0 -21
- data/lib/textus/format/yaml.rb +0 -21
- data/lib/textus/format.rb +16 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -5
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/port/audit_log.rb +6 -6
- data/lib/textus/port/build_lock.rb +1 -1
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/port/sentinel_store.rb +3 -3
- data/lib/textus/port/storage/file_store.rb +23 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +58 -2
- data/lib/textus/port/watcher_lock.rb +2 -2
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +3 -10
- data/lib/textus/store/container.rb +140 -10
- data/lib/textus/store/cursor.rb +1 -1
- data/lib/textus/store/{envelope → entry}/reader.rb +8 -4
- data/lib/textus/store/{envelope → entry}/writer.rb +53 -29
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +5 -3
- data/lib/textus/store/jobs/planner.rb +27 -7
- data/lib/textus/store/jobs/queue.rb +9 -1
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +1 -43
- data/lib/textus/store/jobs/sweep.rb +2 -2
- data/lib/textus/store/{geometry.rb → layout.rb} +19 -3
- data/lib/textus/store.rb +53 -30
- data/lib/textus/surface/cli/runner.rb +8 -9
- data/lib/textus/surface/cli/verb/doctor.rb +3 -2
- data/lib/textus/surface/cli/verb/get.rb +5 -3
- data/lib/textus/surface/cli/verb/put.rb +5 -3
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/surface/mcp/server.rb +20 -31
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +5 -4
- data/lib/textus/value/etag.rb +1 -1
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +36 -16
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +1 -1
- data/lib/textus/workflow/runner.rb +10 -18
- data/lib/textus.rb +0 -64
- metadata +70 -70
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/format/shared.rb +0 -17
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Surface
|
|
3
3
|
module MCP
|
|
4
|
-
# Audit cursor fell off the keep window. Client should re-boot and
|
|
5
|
-
# resume from the new latest_seq.
|
|
6
|
-
class CursorExpired < Textus::Error
|
|
7
|
-
JSONRPC_CODE = -32_002
|
|
8
|
-
|
|
9
|
-
def initialize(message, details: {})
|
|
10
|
-
super("cursor_expired", message, details: details)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
4
|
# Tool execution failed (validation, authorization, IO). Wraps an
|
|
15
5
|
# underlying Textus::Error or generic StandardError.
|
|
16
6
|
class ToolError < Textus::Error
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Surface
|
|
3
|
+
module MCP
|
|
4
|
+
class Projector
|
|
5
|
+
def initialize(view_key: :default)
|
|
6
|
+
@view_key = view_key
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def dispatch(verb_name, inputs:, store:)
|
|
10
|
+
spec = VerbRegistry.for(verb_name.to_sym)
|
|
11
|
+
raise Textus::UsageError.new("unknown verb: #{verb_name}") unless spec
|
|
12
|
+
|
|
13
|
+
bound = Textus::Dispatch::Binder.inputs_from_wire(spec, inputs)
|
|
14
|
+
result = store.public_send(verb_name.to_sym, **bound)
|
|
15
|
+
spec.view(@view_key).call(result, bound)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -11,18 +11,9 @@ module Textus
|
|
|
11
11
|
# execution to Catalog.
|
|
12
12
|
class Server
|
|
13
13
|
def initialize(store:, role: Textus::Value::Role::DEFAULT, stdin: $stdin, stdout: $stdout)
|
|
14
|
-
@store = store
|
|
15
|
-
@role = role
|
|
14
|
+
@store = store.with_role(role)
|
|
16
15
|
@stdin = stdin
|
|
17
16
|
@stdout = stdout
|
|
18
|
-
# Session built eagerly so the contract_etag is captured at server start.
|
|
19
|
-
# Changes to manifest/hooks/schemas after this point are detected as drift.
|
|
20
|
-
@session = Textus::Store::Session.new(
|
|
21
|
-
role: @role,
|
|
22
|
-
cursor: @store.audit_log.latest_seq,
|
|
23
|
-
propose_lane: @store.manifest.policy.propose_lane_for(@role),
|
|
24
|
-
contract_etag: contract_etag_now,
|
|
25
|
-
)
|
|
26
17
|
|
|
27
18
|
@sdk = ::MCP::Server.new(
|
|
28
19
|
name: "textus",
|
|
@@ -34,7 +25,6 @@ module Textus
|
|
|
34
25
|
@sdk.resources_read_handler { |params, server_context:| handle_resource_read(params[:uri].to_s, server_context) }
|
|
35
26
|
end
|
|
36
27
|
|
|
37
|
-
# Runs the stdio line loop; delegates each JSON line to the SDK.
|
|
38
28
|
def run
|
|
39
29
|
@stdin.each_line do |line|
|
|
40
30
|
line = line.strip
|
|
@@ -48,32 +38,28 @@ module Textus
|
|
|
48
38
|
end
|
|
49
39
|
end
|
|
50
40
|
|
|
51
|
-
# Called from every MCP::Tool handler block in Catalog.
|
|
52
|
-
# The SDK parses JSON with symbolize_names: true — all nested keys are symbols.
|
|
53
|
-
# Deep-stringify so Catalog.call receives the string-key format it expects.
|
|
54
41
|
def dispatch(verb_name, args, _server_context)
|
|
55
42
|
str_args = deep_stringify_keys(args)
|
|
56
|
-
@
|
|
57
|
-
result = Catalog.call(verb_name.to_s,
|
|
58
|
-
|
|
43
|
+
@store.check_etag!(contract_etag_now) unless Catalog.read_verbs.include?(verb_name.to_s)
|
|
44
|
+
result = Catalog.call(verb_name.to_s, store: @store, args: str_args)
|
|
45
|
+
@store = @store.advance_cursor(@store.audit_log.latest_seq) if verb_name == :pulse
|
|
46
|
+
@store = @store.with_role(@store.role) if verb_name == :boot
|
|
59
47
|
::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
|
|
60
48
|
rescue Textus::ContractDrift => e
|
|
61
49
|
raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
|
|
62
|
-
rescue CursorExpired => e
|
|
63
|
-
raise_handler_error(e.message, CursorExpired::JSONRPC_CODE)
|
|
50
|
+
rescue Textus::CursorExpired => e
|
|
51
|
+
raise_handler_error(e.message, Textus::CursorExpired::JSONRPC_CODE)
|
|
64
52
|
rescue Textus::Surface::MCP::ToolError => e
|
|
65
53
|
raise_handler_error(e.message, ToolError::JSONRPC_CODE)
|
|
66
54
|
rescue StandardError => e
|
|
67
|
-
raise_handler_error("internal: #{e.
|
|
55
|
+
raise_handler_error("internal: #{e.message}", -32_603)
|
|
68
56
|
end
|
|
69
57
|
|
|
70
58
|
private
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
|
|
60
|
+
# Snapshot at server init against the boot-time manifest. New produced entries
|
|
61
|
+
# added by a later reconcile are invisible until the server restarts — this is
|
|
62
|
+
# intentional: a ContractDrift will gate writes on any mid-session manifest change.
|
|
77
63
|
def build_resources
|
|
78
64
|
machine_lane = @store.manifest.policy.machine_lane
|
|
79
65
|
return [] unless machine_lane
|
|
@@ -84,13 +70,15 @@ module Textus
|
|
|
84
70
|
end
|
|
85
71
|
|
|
86
72
|
def handle_resource_read(uri, _server_context)
|
|
87
|
-
key
|
|
88
|
-
env = @store.
|
|
73
|
+
key = uri.delete_prefix("textus://").tr("/", ".")
|
|
74
|
+
env = @store.get(key:)
|
|
89
75
|
text = env.content.is_a?(Hash) ? JSON.dump(env.content) : (env.body || "").to_s
|
|
90
|
-
mime = mime_for_format(
|
|
76
|
+
mime = mime_for_format(env.format)
|
|
91
77
|
[{ uri: uri, mimeType: mime, text: text }]
|
|
92
78
|
rescue Textus::Error => e
|
|
93
79
|
raise_handler_error("resource read failed: #{e.message}", -32_603)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
raise_handler_error("internal: #{e.message}", -32_603)
|
|
94
82
|
end
|
|
95
83
|
|
|
96
84
|
def contract_etag_now = Textus::Value::Etag.for_contract(@store.root)
|
|
@@ -107,9 +95,10 @@ module Textus
|
|
|
107
95
|
|
|
108
96
|
def mime_for_format(format)
|
|
109
97
|
case format.to_s
|
|
110
|
-
when "json"
|
|
111
|
-
when "yaml"
|
|
112
|
-
|
|
98
|
+
when "json" then "application/json"
|
|
99
|
+
when "yaml" then "application/yaml"
|
|
100
|
+
when "markdown" then "text/markdown"
|
|
101
|
+
else "text/plain"
|
|
113
102
|
end
|
|
114
103
|
end
|
|
115
104
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
module
|
|
3
|
-
# Parses a duration value into whole seconds. Accepts a bare integer (or
|
|
4
|
-
# integer-string) of seconds, or `<n><unit>` with unit s/m/h/d. Returns
|
|
5
|
-
# nil for nil or any unparseable value.
|
|
2
|
+
module Value
|
|
6
3
|
module Duration
|
|
7
4
|
UNIT_SECONDS = { "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }.freeze
|
|
8
5
|
|
|
@@ -13,16 +13,14 @@ module Textus
|
|
|
13
13
|
attribute :format, Types::FormatName
|
|
14
14
|
attribute :etag, Types::String
|
|
15
15
|
attribute :uid, Types::String.optional
|
|
16
|
-
attribute :sources, Types::Array.of(Types::
|
|
16
|
+
attribute :sources, Types::Array.of(Types::Any).optional
|
|
17
17
|
attribute :schema_ref, Types::String.optional
|
|
18
18
|
attribute :meta, Types::Hash.default({}.freeze)
|
|
19
19
|
attribute :body, Types::String.optional
|
|
20
20
|
attribute :content, Types::Any.optional
|
|
21
21
|
attribute :freshness, Types::Any.optional
|
|
22
22
|
|
|
23
|
-
# rubocop:disable Metrics/ParameterLists
|
|
24
23
|
def self.build(key:, mentry:, path:, meta:, body:, etag:, content: nil, freshness: nil)
|
|
25
|
-
# rubocop:enable Metrics/ParameterLists
|
|
26
24
|
new(
|
|
27
25
|
protocol: Textus::PROTOCOL,
|
|
28
26
|
key: key,
|
|
@@ -48,7 +46,10 @@ module Textus
|
|
|
48
46
|
|
|
49
47
|
def self.extract_sources(meta)
|
|
50
48
|
v = meta.is_a?(Hash) ? meta["sources"] : nil
|
|
51
|
-
v.is_a?(Array) && !v.empty?
|
|
49
|
+
return nil unless v.is_a?(Array) && !v.empty?
|
|
50
|
+
|
|
51
|
+
valid = v.select { |s| s.is_a?(String) || (s.is_a?(Hash) && s["key"].is_a?(String)) }
|
|
52
|
+
valid.empty? ? nil : valid
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def with(**attrs) = self.class.new(to_h.merge(attrs))
|
data/lib/textus/value/etag.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Textus
|
|
|
27
27
|
# manifest.yaml, then every hook and schema file. Dir.glob already returns
|
|
28
28
|
# sorted paths (Ruby 3.0+), keeping the digest independent of FS order.
|
|
29
29
|
def self.contract_files(root)
|
|
30
|
-
geom = Textus::Store::
|
|
30
|
+
geom = Textus::Store::Layout.new(root)
|
|
31
31
|
[
|
|
32
32
|
geom.manifest_path,
|
|
33
33
|
*Dir.glob(File.join(geom.hooks_dir, "**", "*.rb")),
|
data/lib/textus/value/result.rb
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
module Textus
|
|
4
2
|
module Value
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
# rubocop:disable Lint/ConstantDefinitionInBlock
|
|
4
|
+
Result = Data.define(:ok, :value, :error) do
|
|
5
|
+
def self.success(value) = new(ok: true, value: value, error: nil)
|
|
6
|
+
|
|
7
|
+
def self.failure(code, message, details: {})
|
|
8
|
+
new(ok: false, value: nil, error: { code: code, message: message, details: details })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.extract(result)
|
|
11
12
|
case result
|
|
12
|
-
when
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
13
|
+
when self
|
|
14
|
+
if result.success?
|
|
15
|
+
result.value
|
|
16
|
+
else
|
|
17
|
+
err = result.error
|
|
18
|
+
raise Textus::ActionError.new(err[:code] || :error, err[:message] || "action failed", details: err[:details] || {})
|
|
19
|
+
end
|
|
20
20
|
else
|
|
21
21
|
result
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def success? = ok
|
|
26
|
+
def failure? = !ok
|
|
27
|
+
|
|
28
|
+
def unwrap
|
|
29
|
+
raise Result::UnwrapError.new(error[:code], error[:message], details: error[:details]) unless ok
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UnwrapError < StandardError
|
|
35
|
+
attr_reader :code, :details
|
|
36
|
+
|
|
37
|
+
def initialize(code, message, details: {})
|
|
38
|
+
super(message)
|
|
39
|
+
@code = code
|
|
40
|
+
@details = details
|
|
41
|
+
end
|
|
42
|
+
end
|
|
24
43
|
end
|
|
44
|
+
# rubocop:enable Lint/ConstantDefinitionInBlock
|
|
25
45
|
end
|
|
26
46
|
end
|