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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +148 -45
  3. data/CHANGELOG.md +102 -0
  4. data/README.md +1 -1
  5. data/SPEC.md +12 -12
  6. data/docs/conventions.md +10 -0
  7. data/lib/textus/application/caps.rb +49 -0
  8. data/lib/textus/application/context.rb +2 -2
  9. data/lib/textus/application/envelope/reader.rb +44 -0
  10. data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
  11. data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
  12. data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
  13. data/lib/textus/application/maintenance/migrate.rb +59 -0
  14. data/lib/textus/application/maintenance/rule_lint.rb +65 -0
  15. data/lib/textus/application/maintenance/zone_mv.rb +60 -0
  16. data/lib/textus/application/maintenance.rb +17 -0
  17. data/lib/textus/application/projection.rb +12 -10
  18. data/lib/textus/application/read/audit.rb +106 -0
  19. data/lib/textus/application/read/blame.rb +91 -0
  20. data/lib/textus/application/read/deps.rb +34 -0
  21. data/lib/textus/application/read/freshness.rb +110 -0
  22. data/lib/textus/application/read/get.rb +75 -0
  23. data/lib/textus/application/read/get_or_refresh.rb +63 -0
  24. data/lib/textus/application/read/list.rb +25 -0
  25. data/lib/textus/application/read/policy_explain.rb +47 -0
  26. data/lib/textus/application/read/published.rb +25 -0
  27. data/lib/textus/application/read/pulse.rb +101 -0
  28. data/lib/textus/application/read/rdeps.rb +35 -0
  29. data/lib/textus/application/read/schema_envelope.rb +26 -0
  30. data/lib/textus/application/read/stale.rb +23 -0
  31. data/lib/textus/application/read/uid.rb +30 -0
  32. data/lib/textus/application/read/validate_all.rb +32 -0
  33. data/lib/textus/application/{reads → read}/validator.rb +2 -2
  34. data/lib/textus/application/read/where.rb +26 -0
  35. data/lib/textus/application/use_case.rb +22 -0
  36. data/lib/textus/application/write/accept.rb +102 -0
  37. data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
  38. data/lib/textus/application/write/delete.rb +45 -0
  39. data/lib/textus/application/{writes → write}/materializer.rb +14 -15
  40. data/lib/textus/application/write/mv.rb +118 -0
  41. data/lib/textus/application/write/publish.rb +96 -0
  42. data/lib/textus/application/write/put.rb +49 -0
  43. data/lib/textus/application/write/refresh_all.rb +63 -0
  44. data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
  45. data/lib/textus/application/write/refresh_worker.rb +134 -0
  46. data/lib/textus/application/write/reject.rb +62 -0
  47. data/lib/textus/boot.rb +27 -29
  48. data/lib/textus/builder/pipeline.rb +3 -3
  49. data/lib/textus/cli/group/mcp.rb +9 -0
  50. data/lib/textus/cli/group/zone.rb +9 -0
  51. data/lib/textus/cli/verb/accept.rb +1 -1
  52. data/lib/textus/cli/verb/audit.rb +2 -2
  53. data/lib/textus/cli/verb/blame.rb +1 -1
  54. data/lib/textus/cli/verb/boot.rb +1 -1
  55. data/lib/textus/cli/verb/build.rb +2 -2
  56. data/lib/textus/cli/verb/delete.rb +1 -1
  57. data/lib/textus/cli/verb/deps.rb +1 -1
  58. data/lib/textus/cli/verb/doctor.rb +1 -1
  59. data/lib/textus/cli/verb/freshness.rb +1 -1
  60. data/lib/textus/cli/verb/get.rb +1 -1
  61. data/lib/textus/cli/verb/hook_run.rb +3 -4
  62. data/lib/textus/cli/verb/hooks.rb +11 -14
  63. data/lib/textus/cli/verb/key_delete.rb +24 -0
  64. data/lib/textus/cli/verb/list.rb +1 -1
  65. data/lib/textus/cli/verb/mcp_serve.rb +17 -0
  66. data/lib/textus/cli/verb/migrate.rb +18 -0
  67. data/lib/textus/cli/verb/mv.rb +11 -3
  68. data/lib/textus/cli/verb/published.rb +1 -1
  69. data/lib/textus/cli/verb/pulse.rb +1 -1
  70. data/lib/textus/cli/verb/put.rb +8 -6
  71. data/lib/textus/cli/verb/rdeps.rb +1 -1
  72. data/lib/textus/cli/verb/refresh.rb +1 -1
  73. data/lib/textus/cli/verb/refresh_stale.rb +1 -1
  74. data/lib/textus/cli/verb/reject.rb +1 -1
  75. data/lib/textus/cli/verb/rule_explain.rb +1 -1
  76. data/lib/textus/cli/verb/rule_lint.rb +18 -0
  77. data/lib/textus/cli/verb/schema.rb +1 -1
  78. data/lib/textus/cli/verb/uid.rb +1 -1
  79. data/lib/textus/cli/verb/where.rb +1 -1
  80. data/lib/textus/cli/verb/zone_mv.rb +19 -0
  81. data/lib/textus/cli/verb.rb +4 -4
  82. data/lib/textus/doctor/check/audit_log.rb +2 -2
  83. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  84. data/lib/textus/doctor/check/hooks.rb +4 -3
  85. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  86. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  87. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  88. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  89. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  90. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  91. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  92. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  93. data/lib/textus/doctor/check/schemas.rb +2 -2
  94. data/lib/textus/doctor/check/sentinels.rb +2 -2
  95. data/lib/textus/doctor/check/templates.rb +2 -2
  96. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  97. data/lib/textus/doctor/check.rb +5 -3
  98. data/lib/textus/doctor.rb +24 -27
  99. data/lib/textus/domain/authorizer.rb +4 -4
  100. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  101. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  102. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  103. data/lib/textus/domain/staleness/generator_check.rb +2 -2
  104. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  105. data/lib/textus/domain/staleness.rb +1 -1
  106. data/lib/textus/hooks/builtin.rb +14 -14
  107. data/lib/textus/hooks/context.rb +13 -13
  108. data/lib/textus/hooks/error_log.rb +32 -0
  109. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  110. data/lib/textus/hooks/loader.rb +29 -3
  111. data/lib/textus/hooks/rpc_registry.rb +77 -0
  112. data/lib/textus/infra/audit_subscriber.rb +6 -7
  113. data/lib/textus/infra/refresh/detached.rb +1 -1
  114. data/lib/textus/key/path.rb +7 -3
  115. data/lib/textus/manifest/data.rb +78 -0
  116. data/lib/textus/manifest/entry/base.rb +4 -4
  117. data/lib/textus/manifest/entry/derived.rb +4 -5
  118. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  119. data/lib/textus/manifest/policy.rb +48 -0
  120. data/lib/textus/manifest/resolver.rb +14 -14
  121. data/lib/textus/manifest/rules.rb +1 -1
  122. data/lib/textus/manifest.rb +53 -111
  123. data/lib/textus/mcp/errors.rb +32 -0
  124. data/lib/textus/mcp/server.rb +127 -0
  125. data/lib/textus/mcp/session.rb +31 -0
  126. data/lib/textus/mcp/tool_schemas.rb +71 -0
  127. data/lib/textus/mcp/tools.rb +129 -0
  128. data/lib/textus/mcp.rb +6 -0
  129. data/lib/textus/schema/tools.rb +14 -10
  130. data/lib/textus/session.rb +84 -0
  131. data/lib/textus/store.rb +14 -9
  132. data/lib/textus/version.rb +1 -1
  133. data/lib/textus.rb +8 -1
  134. metadata +61 -36
  135. data/lib/textus/application/reads/audit.rb +0 -94
  136. data/lib/textus/application/reads/blame.rb +0 -82
  137. data/lib/textus/application/reads/deps.rb +0 -26
  138. data/lib/textus/application/reads/freshness.rb +0 -88
  139. data/lib/textus/application/reads/get.rb +0 -67
  140. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  141. data/lib/textus/application/reads/list.rb +0 -17
  142. data/lib/textus/application/reads/policy_explain.rb +0 -39
  143. data/lib/textus/application/reads/published.rb +0 -17
  144. data/lib/textus/application/reads/pulse.rb +0 -63
  145. data/lib/textus/application/reads/rdeps.rb +0 -27
  146. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  147. data/lib/textus/application/reads/stale.rb +0 -15
  148. data/lib/textus/application/reads/uid.rb +0 -23
  149. data/lib/textus/application/reads/validate_all.rb +0 -24
  150. data/lib/textus/application/reads/where.rb +0 -18
  151. data/lib/textus/application/refresh/all.rb +0 -52
  152. data/lib/textus/application/refresh/worker.rb +0 -116
  153. data/lib/textus/application/writes/accept.rb +0 -89
  154. data/lib/textus/application/writes/delete.rb +0 -33
  155. data/lib/textus/application/writes/mv.rb +0 -105
  156. data/lib/textus/application/writes/publish.rb +0 -81
  157. data/lib/textus/application/writes/put.rb +0 -37
  158. data/lib/textus/application/writes/reject.rb +0 -50
  159. data/lib/textus/infra/event_bus.rb +0 -27
  160. 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
@@ -0,0 +1,6 @@
1
+ module Textus
2
+ # The agent gate. Stdio JSON-RPC 2.0 server speaking MCP draft 2024-11-05.
3
+ # Wraps Textus::Session as auto-derived tools. See ADR 0015.
4
+ module MCP
5
+ end
6
+ end
@@ -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 = Textus::Operations.for(store).get(from)
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 = Textus::Operations.for(store).get(row[:key])
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.manifest.roles_with_kind(:accept_authority).first
53
- if authority.nil?
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, :bus
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
- @bus = Hooks::Bus.new
42
- Infra::AuditSubscriber.new(@audit_log).attach(@bus)
43
- Hooks::Builtin.register_all(@bus)
44
- Hooks::Loader.new(bus: @bus).load_dir(File.join(@root, "hooks"))
45
- ops = Operations.for(self, role: Role::DEFAULT)
46
- @bus.publish(:store_loaded, ctx: ops.hook_context)
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
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.22.0"
2
+ VERSION = "0.26.0"
3
3
  PROTOCOL = "textus/3"
4
4
  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,11 +10,16 @@ loader.inflector.inflect(
8
10
  "json" => "Json",
9
11
  "yaml" => "Yaml",
10
12
  "hook_dsl_scanner" => "HookDSLScanner",
11
- "envelope_io" => "EnvelopeIO",
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.22.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/policy/predicates/accept_authority_signed.rb
114
- - lib/textus/application/policy/predicates/schema_valid.rb
115
- - lib/textus/application/policy/promotion.rb
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/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
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