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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +9 -9
  4. data/SPEC.md +14 -13
  5. data/docs/architecture/README.md +3 -3
  6. data/docs/reference/conventions.md +5 -2
  7. data/lib/textus/boot.rb +64 -85
  8. data/lib/textus/{gate → dispatch}/binder.rb +8 -10
  9. data/lib/textus/dispatch/contracts.rb +63 -0
  10. data/lib/textus/dispatch/handler_registry.rb +21 -0
  11. data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
  12. data/lib/textus/dispatch/middleware/auth.rb +40 -0
  13. data/lib/textus/dispatch/middleware/base.rb +26 -0
  14. data/lib/textus/dispatch/middleware/binder.rb +20 -0
  15. data/lib/textus/dispatch/middleware/cascade.rb +53 -0
  16. data/lib/textus/dispatch/pipeline.rb +35 -0
  17. data/lib/textus/doctor/check/audit_log.rb +1 -1
  18. data/lib/textus/doctor/check/generator_drift.rb +2 -2
  19. data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
  20. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  21. data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
  22. data/lib/textus/doctor/check/sentinels.rb +1 -1
  23. data/lib/textus/doctor/check.rb +8 -6
  24. data/lib/textus/doctor.rb +1 -1
  25. data/lib/textus/errors.rb +2 -0
  26. data/lib/textus/format/base.rb +36 -8
  27. data/lib/textus/format/json.rb +0 -21
  28. data/lib/textus/format/markdown.rb +0 -21
  29. data/lib/textus/format/yaml.rb +0 -21
  30. data/lib/textus/format.rb +16 -1
  31. data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
  32. data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
  33. data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
  34. data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
  35. data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
  36. data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
  37. data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
  38. data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
  39. data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
  40. data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
  41. data/lib/textus/handlers/read/audit_entries.rb +48 -0
  42. data/lib/textus/handlers/read/blame_entry.rb +71 -0
  43. data/lib/textus/handlers/read/deps_entry.rb +17 -0
  44. data/lib/textus/handlers/read/get_entry.rb +68 -0
  45. data/lib/textus/handlers/read/list_keys.rb +36 -0
  46. data/lib/textus/handlers/read/pulse_entries.rb +66 -0
  47. data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
  48. data/lib/textus/handlers/read/uid_entry.rb +18 -0
  49. data/lib/textus/handlers/read/where_entry.rb +18 -0
  50. data/lib/textus/handlers/write/accept_proposal.rb +39 -0
  51. data/lib/textus/handlers/write/data_mv.rb +55 -0
  52. data/lib/textus/handlers/write/delete_key.rb +17 -0
  53. data/lib/textus/handlers/write/enqueue_job.rb +27 -0
  54. data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
  55. data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
  56. data/lib/textus/handlers/write/move_key.rb +80 -0
  57. data/lib/textus/handlers/write/propose_entry.rb +29 -0
  58. data/lib/textus/handlers/write/put_entry.rb +29 -0
  59. data/lib/textus/handlers/write/reject_proposal.rb +29 -0
  60. data/lib/textus/init.rb +5 -5
  61. data/lib/textus/manifest/capabilities.rb +1 -1
  62. data/lib/textus/manifest/entry/base.rb +3 -3
  63. data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
  64. data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
  65. data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
  66. data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
  67. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
  68. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
  69. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
  70. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
  71. data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
  72. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
  73. data/lib/textus/manifest/policy/predicates.rb +54 -0
  74. data/lib/textus/manifest/policy/retention.rb +1 -1
  75. data/lib/textus/orchestration.rb +55 -0
  76. data/lib/textus/port/audit_log.rb +6 -6
  77. data/lib/textus/port/build_lock.rb +1 -1
  78. data/lib/textus/{core → port}/sentinel.rb +1 -6
  79. data/lib/textus/port/sentinel_store.rb +3 -3
  80. data/lib/textus/port/storage/file_store.rb +23 -0
  81. data/lib/textus/port/storage/interface.rb +17 -0
  82. data/lib/textus/port/store.rb +58 -2
  83. data/lib/textus/port/watcher_lock.rb +2 -2
  84. data/lib/textus/produce/engine.rb +1 -11
  85. data/lib/textus/produce/publisher.rb +21 -0
  86. data/lib/textus/schema/registry.rb +42 -0
  87. data/lib/textus/schema/tools.rb +3 -10
  88. data/lib/textus/store/container.rb +140 -10
  89. data/lib/textus/store/cursor.rb +1 -1
  90. data/lib/textus/store/{envelope → entry}/reader.rb +8 -4
  91. data/lib/textus/store/{envelope → entry}/writer.rb +53 -29
  92. data/lib/textus/store/envelope/meta.rb +61 -0
  93. data/lib/textus/store/freshness/drift_detector.rb +93 -0
  94. data/lib/textus/store/freshness/evaluator.rb +20 -0
  95. data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
  96. data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
  97. data/lib/textus/store/freshness.rb +8 -0
  98. data/lib/textus/store/index/builder.rb +5 -3
  99. data/lib/textus/store/jobs/planner.rb +27 -7
  100. data/lib/textus/store/jobs/queue.rb +9 -1
  101. data/lib/textus/store/jobs/retention/base.rb +52 -0
  102. data/lib/textus/store/jobs/retention/sweep.rb +55 -0
  103. data/lib/textus/store/jobs/retention.rb +1 -43
  104. data/lib/textus/store/jobs/sweep.rb +2 -2
  105. data/lib/textus/store/{geometry.rb → layout.rb} +19 -3
  106. data/lib/textus/store.rb +53 -30
  107. data/lib/textus/surface/cli/runner.rb +8 -9
  108. data/lib/textus/surface/cli/verb/doctor.rb +3 -2
  109. data/lib/textus/surface/cli/verb/get.rb +5 -3
  110. data/lib/textus/surface/cli/verb/put.rb +5 -3
  111. data/lib/textus/surface/mcp/catalog.rb +26 -62
  112. data/lib/textus/surface/mcp/errors.rb +0 -10
  113. data/lib/textus/surface/mcp/projector.rb +20 -0
  114. data/lib/textus/surface/mcp/server.rb +20 -31
  115. data/lib/textus/{core → value}/duration.rb +1 -4
  116. data/lib/textus/value/envelope.rb +5 -4
  117. data/lib/textus/value/etag.rb +1 -1
  118. data/lib/textus/value/payload.rb +7 -0
  119. data/lib/textus/value/result.rb +36 -16
  120. data/lib/textus/verb_registry.rb +417 -0
  121. data/lib/textus/version.rb +1 -1
  122. data/lib/textus/workflow/loader.rb +1 -1
  123. data/lib/textus/workflow/runner.rb +10 -18
  124. data/lib/textus.rb +0 -64
  125. metadata +70 -70
  126. data/lib/textus/action/accept.rb +0 -46
  127. data/lib/textus/action/audit.rb +0 -94
  128. data/lib/textus/action/base.rb +0 -42
  129. data/lib/textus/action/blame.rb +0 -79
  130. data/lib/textus/action/boot.rb +0 -15
  131. data/lib/textus/action/data_mv.rb +0 -58
  132. data/lib/textus/action/deps.rb +0 -19
  133. data/lib/textus/action/doctor.rb +0 -17
  134. data/lib/textus/action/drain.rb +0 -31
  135. data/lib/textus/action/enqueue.rb +0 -37
  136. data/lib/textus/action/get.rb +0 -34
  137. data/lib/textus/action/ingest.rb +0 -199
  138. data/lib/textus/action/jobs.rb +0 -27
  139. data/lib/textus/action/key_delete.rb +0 -26
  140. data/lib/textus/action/key_delete_prefix.rb +0 -35
  141. data/lib/textus/action/key_mv.rb +0 -122
  142. data/lib/textus/action/key_mv_prefix.rb +0 -48
  143. data/lib/textus/action/list.rb +0 -28
  144. data/lib/textus/action/propose.rb +0 -42
  145. data/lib/textus/action/published.rb +0 -22
  146. data/lib/textus/action/pulse.rb +0 -49
  147. data/lib/textus/action/put.rb +0 -38
  148. data/lib/textus/action/rdeps.rb +0 -24
  149. data/lib/textus/action/reject.rb +0 -28
  150. data/lib/textus/action/rule_explain.rb +0 -81
  151. data/lib/textus/action/rule_lint.rb +0 -62
  152. data/lib/textus/action/rule_list.rb +0 -38
  153. data/lib/textus/action/schema_envelope.rb +0 -22
  154. data/lib/textus/action/uid.rb +0 -19
  155. data/lib/textus/action/where.rb +0 -21
  156. data/lib/textus/contract/arg.rb +0 -10
  157. data/lib/textus/contract/dsl.rb +0 -88
  158. data/lib/textus/contract/spec.rb +0 -25
  159. data/lib/textus/contract.rb +0 -12
  160. data/lib/textus/core/freshness/evaluator.rb +0 -150
  161. data/lib/textus/core/freshness.rb +0 -11
  162. data/lib/textus/core/retention/sweep.rb +0 -57
  163. data/lib/textus/core/retention.rb +0 -11
  164. data/lib/textus/format/shared.rb +0 -17
  165. data/lib/textus/gate/auth.rb +0 -212
  166. data/lib/textus/gate.rb +0 -92
  167. data/lib/textus/meta.rb +0 -54
  168. data/lib/textus/schemas.rb +0 -54
  169. data/lib/textus/store/compositor.rb +0 -34
  170. data/lib/textus/store/session.rb +0 -37
  171. data/lib/textus/surface/projector.rb +0 -27
  172. 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
- @session.check_etag!(contract_etag_now) unless Catalog.read_verbs.include?(verb_name.to_s)
57
- result = Catalog.call(verb_name.to_s, session: @session, store: @store, args: str_args)
58
- update_session_for(verb_name.to_s)
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.class}: #{e.message}", -32_603)
55
+ raise_handler_error("internal: #{e.message}", -32_603)
68
56
  end
69
57
 
70
58
  private
71
59
 
72
- def update_session_for(verb_name)
73
- @session = @session.advance_cursor(@store.audit_log.latest_seq) if verb_name == "pulse"
74
- @session = @session.with(contract_etag: contract_etag_now) if verb_name == "boot"
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 = uri.delete_prefix("textus://").tr("/", ".")
88
- env = @store.as(@role).get(key)
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(@store.manifest.resolver.resolve(key).entry.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" then "application/json"
111
- when "yaml" then "application/yaml"
112
- else "text/plain"
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 Core
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::String).optional
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? ? v : nil
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))
@@ -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::Geometry.new(root)
30
+ geom = Textus::Store::Layout.new(root)
31
31
  [
32
32
  geom.manifest_path,
33
33
  *Dir.glob(File.join(geom.hooks_dir, "**", "*.rb")),
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textus
4
+ module Value
5
+ Payload = Data.define(:meta, :body, :content)
6
+ end
7
+ end
@@ -1,26 +1,46 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Textus
4
2
  module Value
5
- # Unwraps Dry::Monads results at the Gate seam.
6
- # Every action returns Success(value) or Failure(code:, message:, details:).
7
- # This module converts Failure into an ActionError for surfaces (CLI, MCP)
8
- # that expect exceptions.
9
- module Result
10
- def self.unwrap(result)
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 Dry::Monads::Result::Success then result.value!
13
- when Dry::Monads::Result::Failure
14
- failure = result.failure
15
- raise ActionError.new(
16
- failure[:code] || :internal,
17
- failure[:message] || "action failed",
18
- details: failure[:details] || {},
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