textus 0.20.2 → 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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +148 -45
  3. data/CHANGELOG.md +194 -0
  4. data/README.md +8 -5
  5. data/SPEC.md +54 -15
  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/{intro.rb → boot.rb} +49 -29
  48. data/lib/textus/builder/pipeline.rb +5 -5
  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 +4 -2
  53. data/lib/textus/cli/verb/blame.rb +1 -1
  54. data/lib/textus/cli/verb/boot.rb +13 -0
  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 +17 -0
  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/cli.rb +1 -1
  83. data/lib/textus/doctor/check/audit_log.rb +2 -2
  84. data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
  85. data/lib/textus/doctor/check/hooks.rb +4 -3
  86. data/lib/textus/doctor/check/illegal_keys.rb +2 -2
  87. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  88. data/lib/textus/doctor/check/manifest_files.rb +2 -2
  89. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  90. data/lib/textus/doctor/check/refresh_locks.rb +2 -2
  91. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  92. data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
  93. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  94. data/lib/textus/doctor/check/schemas.rb +2 -2
  95. data/lib/textus/doctor/check/sentinels.rb +2 -2
  96. data/lib/textus/doctor/check/templates.rb +2 -2
  97. data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
  98. data/lib/textus/doctor/check.rb +5 -3
  99. data/lib/textus/doctor.rb +24 -27
  100. data/lib/textus/domain/authorizer.rb +4 -4
  101. data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
  102. data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
  103. data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
  104. data/lib/textus/domain/staleness/generator_check.rb +2 -2
  105. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  106. data/lib/textus/domain/staleness.rb +1 -1
  107. data/lib/textus/errors.rb +16 -0
  108. data/lib/textus/hooks/builtin.rb +14 -14
  109. data/lib/textus/hooks/context.rb +13 -13
  110. data/lib/textus/hooks/error_log.rb +32 -0
  111. data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
  112. data/lib/textus/hooks/loader.rb +29 -3
  113. data/lib/textus/hooks/rpc_registry.rb +77 -0
  114. data/lib/textus/infra/audit_log.rb +126 -16
  115. data/lib/textus/infra/audit_subscriber.rb +6 -7
  116. data/lib/textus/infra/refresh/detached.rb +1 -1
  117. data/lib/textus/key/path.rb +7 -3
  118. data/lib/textus/manifest/data.rb +78 -0
  119. data/lib/textus/manifest/entry/base.rb +44 -7
  120. data/lib/textus/manifest/entry/derived.rb +41 -6
  121. data/lib/textus/manifest/entry/intake.rb +15 -3
  122. data/lib/textus/manifest/entry/leaf.rb +6 -5
  123. data/lib/textus/manifest/entry/nested.rb +42 -3
  124. data/lib/textus/manifest/entry/parser.rb +8 -44
  125. data/lib/textus/manifest/entry/validators/events.rb +2 -2
  126. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -4
  127. data/lib/textus/manifest/entry/validators/index_filename.rb +2 -1
  128. data/lib/textus/manifest/entry/validators/inject_boot.rb +19 -0
  129. data/lib/textus/manifest/entry/validators/publish_each.rb +4 -3
  130. data/lib/textus/manifest/entry/validators.rb +1 -1
  131. data/lib/textus/manifest/entry.rb +3 -0
  132. data/lib/textus/manifest/policy.rb +48 -0
  133. data/lib/textus/manifest/resolver.rb +18 -18
  134. data/lib/textus/manifest/rules.rb +1 -1
  135. data/lib/textus/manifest/schema.rb +20 -6
  136. data/lib/textus/manifest.rb +53 -101
  137. data/lib/textus/mcp/errors.rb +32 -0
  138. data/lib/textus/mcp/server.rb +127 -0
  139. data/lib/textus/mcp/session.rb +31 -0
  140. data/lib/textus/mcp/tool_schemas.rb +71 -0
  141. data/lib/textus/mcp/tools.rb +129 -0
  142. data/lib/textus/mcp.rb +6 -0
  143. data/lib/textus/schema/tools.rb +14 -10
  144. data/lib/textus/session.rb +84 -0
  145. data/lib/textus/store.rb +17 -8
  146. data/lib/textus/version.rb +1 -1
  147. data/lib/textus.rb +8 -1
  148. metadata +65 -38
  149. data/lib/textus/application/reads/audit.rb +0 -69
  150. data/lib/textus/application/reads/blame.rb +0 -82
  151. data/lib/textus/application/reads/deps.rb +0 -26
  152. data/lib/textus/application/reads/freshness.rb +0 -88
  153. data/lib/textus/application/reads/get.rb +0 -67
  154. data/lib/textus/application/reads/get_or_refresh.rb +0 -51
  155. data/lib/textus/application/reads/list.rb +0 -17
  156. data/lib/textus/application/reads/policy_explain.rb +0 -39
  157. data/lib/textus/application/reads/published.rb +0 -17
  158. data/lib/textus/application/reads/rdeps.rb +0 -27
  159. data/lib/textus/application/reads/schema_envelope.rb +0 -18
  160. data/lib/textus/application/reads/stale.rb +0 -15
  161. data/lib/textus/application/reads/uid.rb +0 -23
  162. data/lib/textus/application/reads/validate_all.rb +0 -24
  163. data/lib/textus/application/reads/where.rb +0 -18
  164. data/lib/textus/application/refresh/all.rb +0 -52
  165. data/lib/textus/application/refresh/worker.rb +0 -116
  166. data/lib/textus/application/writes/accept.rb +0 -89
  167. data/lib/textus/application/writes/delete.rb +0 -33
  168. data/lib/textus/application/writes/mv.rb +0 -105
  169. data/lib/textus/application/writes/publish.rb +0 -162
  170. data/lib/textus/application/writes/put.rb +0 -37
  171. data/lib/textus/application/writes/reject.rb +0 -50
  172. data/lib/textus/cli/verb/intro.rb +0 -13
  173. data/lib/textus/infra/event_bus.rb +0 -27
  174. data/lib/textus/manifest/entry/validators/inject_intro.rb +0 -21
  175. data/lib/textus/operations.rb +0 -169
@@ -0,0 +1,71 @@
1
+ module Textus
2
+ module MCP
3
+ # JSON-Schema definitions for every MCP tool's inputSchema. Returned by
4
+ # the server in tools/list. Static today — a follow-up will enrich with
5
+ # manifest-derived enums for `zone`, `key`, etc.
6
+ module ToolSchemas
7
+ module_function
8
+
9
+ def all # rubocop:disable Metrics/MethodLength
10
+ [
11
+ tool("boot", "Return the orientation contract: zones, entries, schemas, write_flows, agent_quickstart.", {}, []),
12
+ tool("tick", "Delta since cursor. Returns {cursor, changed, stale, pending_review, doctor}.",
13
+ { "since" => { "type" => "integer", "minimum" => 0 } }, []),
14
+ tool("find", "List keys filtered by zone and/or prefix.",
15
+ { "zone" => { "type" => "string" }, "prefix" => { "type" => "string" } }, []),
16
+ tool("read", "Read one entry. Returns the envelope (uid, etag, _meta, body, freshness).",
17
+ { "key" => { "type" => "string" } }, ["key"]),
18
+ tool("write", "Create or update an entry. Schema-validated. Returns {uid, etag}.",
19
+ {
20
+ "key" => { "type" => "string" },
21
+ "meta" => { "type" => "object" },
22
+ "body" => { "type" => "string" },
23
+ "content" => { "type" => "object" },
24
+ "if_etag" => { "type" => "string" },
25
+ }, %w[key meta]),
26
+ tool("propose", "Write a proposal to the session's propose_zone. Auto-prefixes the key.",
27
+ {
28
+ "key" => { "type" => "string", "description" => "Key relative to propose_zone, e.g. 'proposal.feature-x'" },
29
+ "meta" => { "type" => "object" },
30
+ "body" => { "type" => "string" },
31
+ }, %w[key meta]),
32
+ tool("refresh", "Run an intake refresh for one key. Returns the refresh Outcome.",
33
+ { "key" => { "type" => "string" } }, ["key"]),
34
+ tool("refresh_stale", "Refresh all stale intake entries, optionally scoped by zone/prefix.",
35
+ {
36
+ "zone" => { "type" => "string" },
37
+ "prefix" => { "type" => "string" },
38
+ }, []),
39
+ tool("schema", "Return the schema (field shape) for an entry family.",
40
+ { "family" => { "type" => "string" } }, ["family"]),
41
+ tool("rules", "Return effective rules for a key (refresh, promote, ...).",
42
+ { "key" => { "type" => "string" } }, ["key"]),
43
+ tool("key_mv_prefix",
44
+ "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false.",
45
+ { "from_prefix" => { "type" => "string" }, "to_prefix" => { "type" => "string" }, "dry_run" => { "type" => "boolean" } },
46
+ %w[from_prefix to_prefix]),
47
+ tool("key_delete_prefix", "Bulk-delete every leaf key under prefix.",
48
+ { "prefix" => { "type" => "string" }, "dry_run" => { "type" => "boolean" } },
49
+ ["prefix"]),
50
+ tool("zone_mv", "Rename a zone — manifest + files. Refuses if destination exists.",
51
+ { "from" => { "type" => "string" }, "to" => { "type" => "string" }, "dry_run" => { "type" => "boolean" } },
52
+ %w[from to]),
53
+ tool("rule_lint", "Diff candidate manifest YAML's rules against the live manifest. No writes.",
54
+ { "candidate_yaml" => { "type" => "string" } },
55
+ ["candidate_yaml"]),
56
+ tool("migrate", "Run a YAML migration plan (multi-op).",
57
+ { "plan_yaml" => { "type" => "string" }, "dry_run" => { "type" => "boolean" } },
58
+ ["plan_yaml"]),
59
+ ].freeze
60
+ end
61
+
62
+ def tool(name, description, properties, required)
63
+ {
64
+ name: name,
65
+ description: description,
66
+ inputSchema: { type: "object", properties: properties, required: required },
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -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)
@@ -33,13 +33,22 @@ module Textus
33
33
  @manifest = Manifest.load(@root)
34
34
  @schemas = Schemas.new(File.join(@root, "schemas"))
35
35
  @file_store = Infra::Storage::FileStore.new
36
- @audit_log = Infra::AuditLog.new(@root)
37
- @bus = Hooks::Bus.new
38
- Infra::AuditSubscriber.new(@audit_log).attach(@bus)
39
- Hooks::Builtin.register_all(@bus)
40
- Hooks::Loader.new(bus: @bus).load_dir(File.join(@root, "hooks"))
41
- ops = Operations.for(self, role: Role::DEFAULT)
42
- @bus.publish(:store_loaded, ctx: ops.hook_context)
36
+ @audit_log = Infra::AuditLog.new(
37
+ @root,
38
+ max_size: @manifest.data.audit_config[:max_size],
39
+ keep: @manifest.data.audit_config[:keep],
40
+ )
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)
43
52
  end
44
53
  end
45
54
  end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.20.2"
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.20.2
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -109,39 +109,47 @@ 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/rdeps.rb
127
- - lib/textus/application/reads/schema_envelope.rb
128
- - lib/textus/application/reads/stale.rb
129
- - lib/textus/application/reads/uid.rb
130
- - lib/textus/application/reads/validate_all.rb
131
- - lib/textus/application/reads/validator.rb
132
- - lib/textus/application/reads/where.rb
133
- - lib/textus/application/refresh/all.rb
134
- - lib/textus/application/refresh/orchestrator.rb
135
- - lib/textus/application/refresh/worker.rb
136
- - lib/textus/application/writes/accept.rb
137
- - lib/textus/application/writes/authority_gate.rb
138
- - lib/textus/application/writes/delete.rb
139
- - lib/textus/application/writes/envelope_io.rb
140
- - lib/textus/application/writes/materializer.rb
141
- - lib/textus/application/writes/mv.rb
142
- - lib/textus/application/writes/publish.rb
143
- - lib/textus/application/writes/put.rb
144
- - 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
152
+ - lib/textus/boot.rb
145
153
  - lib/textus/builder/pipeline.rb
146
154
  - lib/textus/builder/renderer.rb
147
155
  - lib/textus/builder/renderer/json.rb
@@ -152,13 +160,16 @@ files:
152
160
  - lib/textus/cli/group.rb
153
161
  - lib/textus/cli/group/hook.rb
154
162
  - lib/textus/cli/group/key.rb
163
+ - lib/textus/cli/group/mcp.rb
155
164
  - lib/textus/cli/group/refresh.rb
156
165
  - lib/textus/cli/group/rule.rb
157
166
  - lib/textus/cli/group/schema.rb
167
+ - lib/textus/cli/group/zone.rb
158
168
  - lib/textus/cli/verb.rb
159
169
  - lib/textus/cli/verb/accept.rb
160
170
  - lib/textus/cli/verb/audit.rb
161
171
  - lib/textus/cli/verb/blame.rb
172
+ - lib/textus/cli/verb/boot.rb
162
173
  - lib/textus/cli/verb/build.rb
163
174
  - lib/textus/cli/verb/delete.rb
164
175
  - lib/textus/cli/verb/deps.rb
@@ -168,16 +179,20 @@ files:
168
179
  - lib/textus/cli/verb/hook_run.rb
169
180
  - lib/textus/cli/verb/hooks.rb
170
181
  - lib/textus/cli/verb/init.rb
171
- - lib/textus/cli/verb/intro.rb
182
+ - lib/textus/cli/verb/key_delete.rb
172
183
  - lib/textus/cli/verb/list.rb
184
+ - lib/textus/cli/verb/mcp_serve.rb
185
+ - lib/textus/cli/verb/migrate.rb
173
186
  - lib/textus/cli/verb/mv.rb
174
187
  - lib/textus/cli/verb/published.rb
188
+ - lib/textus/cli/verb/pulse.rb
175
189
  - lib/textus/cli/verb/put.rb
176
190
  - lib/textus/cli/verb/rdeps.rb
177
191
  - lib/textus/cli/verb/refresh.rb
178
192
  - lib/textus/cli/verb/refresh_stale.rb
179
193
  - lib/textus/cli/verb/reject.rb
180
194
  - lib/textus/cli/verb/rule_explain.rb
195
+ - lib/textus/cli/verb/rule_lint.rb
181
196
  - lib/textus/cli/verb/rule_list.rb
182
197
  - lib/textus/cli/verb/schema.rb
183
198
  - lib/textus/cli/verb/schema_diff.rb
@@ -185,6 +200,7 @@ files:
185
200
  - lib/textus/cli/verb/schema_migrate.rb
186
201
  - lib/textus/cli/verb/uid.rb
187
202
  - lib/textus/cli/verb/where.rb
203
+ - lib/textus/cli/verb/zone_mv.rb
188
204
  - lib/textus/doctor.rb
189
205
  - lib/textus/doctor/check.rb
190
206
  - lib/textus/doctor/check/audit_log.rb
@@ -212,7 +228,10 @@ files:
212
228
  - lib/textus/domain/permission.rb
213
229
  - lib/textus/domain/policy/handler_allowlist.rb
214
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
215
233
  - lib/textus/domain/policy/promote.rb
234
+ - lib/textus/domain/policy/promotion.rb
216
235
  - lib/textus/domain/policy/refresh.rb
217
236
  - lib/textus/domain/sentinel.rb
218
237
  - lib/textus/domain/staleness.rb
@@ -228,25 +247,26 @@ files:
228
247
  - lib/textus/errors.rb
229
248
  - lib/textus/etag.rb
230
249
  - lib/textus/hooks/builtin.rb
231
- - lib/textus/hooks/bus.rb
232
250
  - lib/textus/hooks/context.rb
251
+ - lib/textus/hooks/error_log.rb
252
+ - lib/textus/hooks/event_bus.rb
233
253
  - lib/textus/hooks/fire_report.rb
234
254
  - lib/textus/hooks/loader.rb
255
+ - lib/textus/hooks/rpc_registry.rb
235
256
  - lib/textus/infra/audit_log.rb
236
257
  - lib/textus/infra/audit_subscriber.rb
237
258
  - lib/textus/infra/build_lock.rb
238
259
  - lib/textus/infra/clock.rb
239
- - lib/textus/infra/event_bus.rb
240
260
  - lib/textus/infra/publisher.rb
241
261
  - lib/textus/infra/refresh/detached.rb
242
262
  - lib/textus/infra/refresh/lock.rb
243
263
  - lib/textus/infra/storage/file_store.rb
244
264
  - lib/textus/init.rb
245
- - lib/textus/intro.rb
246
265
  - lib/textus/key/distance.rb
247
266
  - lib/textus/key/grammar.rb
248
267
  - lib/textus/key/path.rb
249
268
  - lib/textus/manifest.rb
269
+ - lib/textus/manifest/data.rb
250
270
  - lib/textus/manifest/entry.rb
251
271
  - lib/textus/manifest/entry/base.rb
252
272
  - lib/textus/manifest/entry/derived.rb
@@ -258,18 +278,25 @@ files:
258
278
  - lib/textus/manifest/entry/validators/events.rb
259
279
  - lib/textus/manifest/entry/validators/format_matrix.rb
260
280
  - lib/textus/manifest/entry/validators/index_filename.rb
261
- - lib/textus/manifest/entry/validators/inject_intro.rb
281
+ - lib/textus/manifest/entry/validators/inject_boot.rb
262
282
  - lib/textus/manifest/entry/validators/publish_each.rb
283
+ - lib/textus/manifest/policy.rb
263
284
  - lib/textus/manifest/resolver.rb
264
285
  - lib/textus/manifest/role_kinds.rb
265
286
  - lib/textus/manifest/rules.rb
266
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
267
294
  - lib/textus/mustache.rb
268
- - lib/textus/operations.rb
269
295
  - lib/textus/role.rb
270
296
  - lib/textus/schema.rb
271
297
  - lib/textus/schema/tools.rb
272
298
  - lib/textus/schemas.rb
299
+ - lib/textus/session.rb
273
300
  - lib/textus/store.rb
274
301
  - lib/textus/uid.rb
275
302
  - lib/textus/version.rb