textus 0.55.1 → 0.56.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 (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -3,20 +3,22 @@ module Textus
3
3
  class CLI
4
4
  class Verb
5
5
  class Put < Runner::Base
6
- self.spec = Textus::Action::Put.contract
6
+ self.spec = Textus::Protocol::VerbRegistry.for(:put)
7
7
  option :as_flag, "--as=ROLE"
8
8
  option :use_stdin, "--stdin"
9
+ option :as_format, "--as-format=FORMAT"
9
10
 
10
11
  def invoke(store)
11
12
  key = positional.shift or raise UsageError.new("put requires a key")
12
13
  raise UsageError.new("put requires --stdin in v1") unless use_stdin
13
14
 
14
15
  payload = JSON.parse(@stdin.read)
15
- spec = Textus::Action::Put.contract
16
- inputs = { key: key, meta: payload["_meta"] || {}, body: payload["body"] || "",
17
- content: nil, if_etag: payload["if_etag"] }
18
- result = store.gate.dispatch(spec: spec, inputs: inputs, role: resolved_role(store), surface: :cli)
19
- emit(result)
16
+ Textus::Protocol::VerbRegistry.for(:put)
17
+ inputs = { key: key, meta: payload["_meta"] || {}, data: payload["data"],
18
+ if_etag: payload["if_etag"] }
19
+ inputs[:format] = as_format if as_format
20
+ s = store.with_role(resolved_role(store))
21
+ emit(s.put(**inputs))
20
22
  end
21
23
  end
22
24
  end
@@ -0,0 +1,22 @@
1
+ module Textus
2
+ module Surface
3
+ class CLI
4
+ class Verb
5
+ class WebServe < Verb
6
+ command_name "serve"
7
+ parent_group Group::Web
8
+ option :as_flag, "--as=ROLE"
9
+ option :port, "--port=N"
10
+
11
+ def call(store)
12
+ role = resolved_role(store, default: Textus::Value::Role::AGENT)
13
+ Textus::Surface::Web.set :textus_session, store.with_role(role)
14
+ Textus::Surface::Web.set :port, (port || 9292).to_i
15
+ Textus::Surface::Web.run!
16
+ 0
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -40,7 +40,7 @@ module Textus
40
40
  # `textus hook list --root=X`). Without this, `order!` below only sees
41
41
  # options before the first verb token, so a trailing `--root` reached the
42
42
  # verb's own parser and raised InvalidOption (#161 F5). TEXTUS_ROOT already
43
- # works everywhere via Store.discover, so this brings the flag to parity.
43
+ # works everywhere via Session, so this brings the flag to parity.
44
44
  @root_arg = extract_root!(argv)
45
45
 
46
46
  # Define --version/--help ourselves so OptionParser doesn't intercept them
@@ -99,7 +99,29 @@ module Textus
99
99
  end
100
100
 
101
101
  def store
102
- @store ||= Store.discover(@cwd, root: @root_arg)
102
+ @store ||= Textus::Protocol::Session.open(store_root)
103
+ end
104
+
105
+ def store_root
106
+ @store_root ||= explicit_root || discovered_root
107
+ end
108
+
109
+ def explicit_root
110
+ @root_arg || ENV.fetch("TEXTUS_ROOT", nil)
111
+ end
112
+
113
+ def discovered_root
114
+ dir = File.expand_path(@cwd)
115
+ loop do
116
+ if File.directory?(File.join(dir, ".textus")) && File.exist?(File.join(dir, ".textus", "manifest.yaml"))
117
+ return File.join(dir, ".textus")
118
+ end
119
+
120
+ parent = File.dirname(dir)
121
+ raise Textus::IoError.new("no .textus directory found from #{@cwd}") if parent == dir
122
+
123
+ dir = parent
124
+ end
103
125
  end
104
126
 
105
127
  def dispatch(klass, argv)
@@ -131,14 +153,11 @@ module Textus
131
153
  textus blame KEY [--limit=N]
132
154
  textus pulse [--since=N]
133
155
  textus boot
134
- textus doctor
135
156
  textus drain [PREFIX] --as=ROLE
136
157
  textus watch
137
- textus jobs
138
-
139
158
  textus key {delete,mv,mv-prefix,delete-prefix,uid}
140
159
  textus rule {explain,lint,list}
141
- textus schema {diff,init,migrate,show}
160
+ textus schema {show}
142
161
  textus data {mv}
143
162
  textus mcp {serve}
144
163
  HELP
@@ -0,0 +1,17 @@
1
+ module Textus
2
+ module Surface
3
+ module Dispatch
4
+ module_function
5
+
6
+ def call(session, verb_name, raw_args = nil, **bound)
7
+ if raw_args
8
+ spec = Textus::Protocol::VerbRegistry.for(verb_name.to_sym)
9
+ raise Textus::UsageError.new("unknown verb: #{verb_name}") unless spec
10
+
11
+ bound = Textus::Protocol::Binder.inputs_from_wire(spec, raw_args)
12
+ end
13
+ session.public_send(verb_name.to_sym, **bound)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Textus
2
+ module Surface
3
+ module MCP
4
+ module Adapters
5
+ class McpAdapter
6
+ def server(name:, version:, tools:, resources:, server_context:)
7
+ ::MCP::Server.new(
8
+ name: name,
9
+ version: version,
10
+ tools: tools,
11
+ resources: resources,
12
+ server_context: server_context,
13
+ )
14
+ end
15
+
16
+ def tool(name:, description:, input_schema:, &)
17
+ ::MCP::Tool.define(
18
+ name: name,
19
+ description: description,
20
+ input_schema: input_schema,
21
+ &
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,89 +1,53 @@
1
1
  module Textus
2
2
  module Surface
3
3
  module MCP
4
- # Derives the entire MCP tool surface from the per-verb contracts (ADR 0039).
5
- # `build_tools` builds MCP::Tool instances for the SDK; `call` is the generic
6
- # dispatch: map JSON args -> (positional, keyword) per the contract, invoke
7
- # the verb through the role scope, then shape the return value. No per-tool code.
8
4
  module Catalog
9
- PROJECTOR = Projector.new(view_key: :default, binder_method: :inputs_from_wire).freeze
5
+ MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
10
6
 
11
7
  module_function
12
8
 
13
- WRITE_VERBS = %i[
14
- put propose key_delete key_mv accept reject enqueue
15
- ].freeze
16
-
17
- MAINTENANCE_VERBS = %i[
18
- data_mv key_mv_prefix key_delete_prefix drain rule_lint
19
- ].freeze
20
-
21
- # Contracts of every MCP-surfaced verb, in Dispatcher order.
22
9
  def specs
23
- Textus::Action::VERBS.values
24
- .select { |k| mcp_surfaced?(k) }
25
- .map(&:contract)
10
+ Textus::Protocol::VerbRegistry.registered.select(&:mcp?)
26
11
  end
27
12
 
28
- # Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
29
13
  def build_tools(mcp_server)
30
- Textus::Action::VERBS
31
- .select { |_, klass| mcp_surfaced?(klass) }
32
- .map do |name, action|
33
- schema = action.contract.input_schema
34
- schema = schema.reject { |k, v| k == :required && Array(v).empty? }
35
- ::MCP::Tool.define(
36
- name: name.to_s,
37
- description: action.contract.summary,
38
- input_schema: schema,
39
- ) do |server_context:, **args|
40
- mcp_server.dispatch(name, args, server_context)
41
- end
14
+ specs.map do |spec|
15
+ schema = spec.input_schema
16
+ schema = schema.reject { |k, v| k == :required && Array(v).empty? }
17
+ MCP_ADAPTER.tool(
18
+ name: spec.verb.to_s,
19
+ description: spec.summary,
20
+ input_schema: schema,
21
+ ) do |server_context:, **args|
22
+ mcp_server.dispatch(spec.verb, args, server_context)
42
23
  end
24
+ end
43
25
  end
44
26
 
45
27
  def names
46
- PROJECTOR.names(
47
- Textus::Action::VERBS.select { |_, klass| mcp_surfaced?(klass) },
48
- )
28
+ specs.map(&:verb).map(&:to_s)
49
29
  end
50
30
 
51
- # MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
52
- # real read/discovery surface. `boot.agent_quickstart.read_verbs` derives
53
- # from this so it can never advertise a verb the agent cannot call, nor
54
- # omit one it can (ADR 0056). Excludes write/maintenance verbs by verb
55
- # identity (routing may be legacy UseCases or Dispatch::Actions).
56
31
  def read_verbs
57
- Textus::Action::VERBS
58
- .reject { |verb, _klass| WRITE_VERBS.include?(verb) || MAINTENANCE_VERBS.include?(verb) }
59
- .select { |_verb, klass| mcp_surfaced?(klass) }
60
- .keys.map(&:to_s)
32
+ Textus::Protocol::VerbRegistry.registered
33
+ .select { |s| s.read? && s.mcp? }
34
+ .map { |s| s.verb.to_s }
61
35
  end
62
36
 
63
- # MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of
64
- # read_verbs for the write side. `boot.agent_quickstart.write_verbs` derives
65
- # from this so it advertises bare verb names the agent can call (no `--as`/
66
- # `--stdin` CLI framing), finishing the de-CLI-ing of the agent surface
67
- # (ADR 0056, ADR 0057).
68
37
  def write_verbs
69
- Textus::Action::VERBS
70
- .select { |verb, klass| WRITE_VERBS.include?(verb) && mcp_surfaced?(klass) }
71
- .keys.map(&:to_s)
72
- end
73
-
74
- def mcp_surfaced?(klass)
75
- klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
38
+ Textus::Protocol::VerbRegistry.registered
39
+ .select { |s| s.write? && s.mcp? }
40
+ .map { |s| s.verb.to_s }
76
41
  end
77
42
 
78
- def call(name, session:, store:, args:)
79
- klass = Textus::Action::VERBS[name.to_sym]
80
- raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)
43
+ def call(name, store:, args:)
44
+ spec = Textus::Protocol::VerbRegistry.for(name.to_sym)
45
+ raise ToolError.new("unknown tool: #{name}") unless spec&.mcp?
81
46
 
82
- PROJECTOR.dispatch(name, inputs: args, store:, role: session.role, session:)
83
- rescue Textus::Gate::MissingArgs => e
84
- spec = klass.contract
85
- raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
86
- rescue Textus::ContractDrift, CursorExpired
47
+ Textus::Surface::Dispatch.call(store, name.to_sym, args)
48
+ rescue Textus::Protocol::MissingArgs => e
49
+ raise ToolError.new("#{name}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
50
+ rescue Textus::ContractDrift, Textus::CursorExpired
87
51
  raise
88
52
  rescue Textus::Error => e
89
53
  raise ToolError.new("#{name}: #{e.message}")
@@ -0,0 +1,22 @@
1
+ module Textus
2
+ module Surface
3
+ module MCP
4
+ class CursorManager
5
+ def initialize(store:, role:)
6
+ @store = store
7
+ @role = role
8
+ end
9
+
10
+ def resume!
11
+ saved = @store.container.store.cursor_store.get(@role)[:seq]
12
+ latest = @store.container.store.audit_log.latest[:seq]
13
+ @store.advance_cursor(saved) if saved.positive? && saved <= latest
14
+ end
15
+
16
+ def advance!(seq:)
17
+ @store.advance_cursor(seq)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Surface
3
+ module MCP
4
+ module DriftAnnotation
5
+ module_function
6
+
7
+ def apply(result, verb_name:, drifted:, is_read:, current_etag:, previous_etag:)
8
+ result["contract_drifted"] = drifted if verb_name == :pulse
9
+
10
+ if drifted && !is_read && verb_name != :boot
11
+ result["_warning"] = "contract drifted (was #{previous_etag}, now #{current_etag}); call boot() to re-arm"
12
+ end
13
+
14
+ result
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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
@@ -1,30 +1,22 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "mcp"
4
2
 
5
3
  module Textus
6
4
  module Surface
7
5
  module MCP
8
- # MCP stdio server backed by the official mcp gem. The SDK owns protocol
9
- # negotiation, tool dispatch, and JSON-RPC framing. This class owns the
10
- # textus Session lifecycle (built lazily on first tool call) and delegates
11
- # execution to Catalog.
12
6
  class Server
7
+ CHECKPOINT_INTERVAL = 30
8
+ MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
9
+
13
10
  def initialize(store:, role: Textus::Value::Role::DEFAULT, stdin: $stdin, stdout: $stdout)
14
- @store = store
15
- @role = role
11
+ @store = store.with_role(role)
16
12
  @stdin = stdin
17
13
  @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
- )
14
+ @shutdown = false
15
+ @cursor_manager = CursorManager.new(store: @store, role:)
26
16
 
27
- @sdk = ::MCP::Server.new(
17
+ @store = @cursor_manager.resume! || @store
18
+
19
+ @sdk = MCP_ADAPTER.server(
28
20
  name: "textus",
29
21
  version: Textus::VERSION,
30
22
  tools: Catalog.build_tools(self),
@@ -34,7 +26,6 @@ module Textus
34
26
  @sdk.resources_read_handler { |params, server_context:| handle_resource_read(params[:uri].to_s, server_context) }
35
27
  end
36
28
 
37
- # Runs the stdio line loop; delegates each JSON line to the SDK.
38
29
  def run
39
30
  @stdin.each_line do |line|
40
31
  line = line.strip
@@ -48,55 +39,63 @@ module Textus
48
39
  end
49
40
  end
50
41
 
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
42
  def dispatch(verb_name, args, _server_context)
55
43
  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)
44
+ drifted = drift_detected?
45
+
46
+ result = Catalog.call(verb_name.to_s, store: @store, args: str_args)
47
+ result = DriftAnnotation.apply(
48
+ result, verb_name:, drifted:, is_read: Catalog.read_verbs.include?(verb_name.to_s),
49
+ current_etag: short_etag(contract_etag_now),
50
+ previous_etag: short_etag(@store.contract_etag)
51
+ )
52
+
53
+ post_dispatch!(verb_name)
59
54
  ::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
60
- rescue Textus::ContractDrift => e
61
- raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
62
- rescue CursorExpired => e
63
- raise_handler_error(e.message, CursorExpired::JSONRPC_CODE)
55
+ rescue Textus::CursorExpired => e
56
+ raise_handler_error(e.message, Textus::CursorExpired::JSONRPC_CODE)
64
57
  rescue Textus::Surface::MCP::ToolError => e
65
58
  raise_handler_error(e.message, ToolError::JSONRPC_CODE)
66
59
  rescue StandardError => e
67
- raise_handler_error("internal: #{e.class}: #{e.message}", -32_603)
60
+ raise_handler_error("internal: #{e.message}", -32_603)
68
61
  end
69
62
 
70
63
  private
71
64
 
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"
65
+ def drift_detected?
66
+ @store.contract_etag != contract_etag_now
67
+ end
68
+
69
+ def post_dispatch!(verb_name)
70
+ @store = @cursor_manager.advance!(seq: @store.container.store.audit_log.latest[:seq]) if verb_name == :pulse
71
+ @store = @store.with_role(@store.role) if verb_name == :boot
75
72
  end
76
73
 
77
74
  def build_resources
78
- machine_lane = @store.manifest.policy.machine_lane
75
+ machine_lane = @store.respond_to?(:container) ? @store.container.manifest.policy.machine_lane : nil
79
76
  return [] unless machine_lane
80
77
 
81
- @store.manifest.data.entries
82
- .select { |e| e.lane == machine_lane && e.is_a?(Textus::Manifest::Entry::Produced) }
83
- .map { |e| ::MCP::Resource.new(uri: "textus://#{e.key.tr(".", "/")}", name: e.key, mime_type: mime_for_format(e.format)) }
78
+ entries = @store.respond_to?(:container) ? @store.container.manifest.data.entries : []
79
+ entries.select { |e| e.lane == machine_lane }
80
+ .map { |e| ::MCP::Resource.new(uri: "textus://#{e.key.tr(".", "/")}", name: e.key, mime_type: mime_for_format(e.format)) }
84
81
  end
85
82
 
86
83
  def handle_resource_read(uri, _server_context)
87
- key = uri.delete_prefix("textus://").tr("/", ".")
88
- env = @store.as(@role).get(key)
89
- 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)
84
+ key = uri.delete_prefix("textus://").tr("/", ".")
85
+ env = @store.get(key:)
86
+ text = env["content"].is_a?(Hash) ? JSON.dump(env["content"]) : (env["body"] || "").to_s
87
+ mime = mime_for_format(env["format"])
91
88
  [{ uri: uri, mimeType: mime, text: text }]
92
89
  rescue Textus::Error => e
93
90
  raise_handler_error("resource read failed: #{e.message}", -32_603)
91
+ rescue StandardError => e
92
+ raise_handler_error("internal: #{e.message}", -32_603)
94
93
  end
95
94
 
96
- def contract_etag_now = Textus::Value::Etag.for_contract(@store.root)
95
+ def contract_etag_now = Textus::Value::Etag.for_contract(@store.layout)
96
+
97
+ def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
97
98
 
98
- # The SDK parses JSON with symbolize_names:true, making all nested hash keys symbols.
99
- # Recursively stringify so Catalog.call receives string-keyed hashes throughout.
100
99
  def deep_stringify_keys(obj)
101
100
  case obj
102
101
  when Hash then obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
@@ -107,9 +106,10 @@ module Textus
107
106
 
108
107
  def mime_for_format(format)
109
108
  case format.to_s
110
- when "json" then "application/json"
111
- when "yaml" then "application/yaml"
112
- else "text/plain"
109
+ when "json" then "application/json"
110
+ when "yaml" then "application/yaml"
111
+ when "markdown" then "text/markdown"
112
+ else "text/plain"
113
113
  end
114
114
  end
115
115
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "securerandom"
4
2
 
5
3
  module Textus
@@ -7,22 +5,30 @@ module Textus
7
5
  class Watcher
8
6
  def initialize(container:)
9
7
  @container = container
8
+ @registry = Textus::Workflow::Registry.new(container.workflows)
10
9
  end
11
10
 
12
11
  def tick
13
- queue = Textus::Store::Jobs::Queue.new(store: @container.job_store)
14
- Textus::Store::Jobs::Planner.seed(
15
- container: @container,
16
- queue: queue,
17
- role: Textus::Value::Role::AUTOMATION,
18
- )
19
- queue.reclaim(now: Textus::Port::Clock.new.now)
20
- Textus::Store::Jobs::Worker.for(container: @container, queue: queue).drain
12
+ call = Textus::Value::Call.build(role: Textus::Value::Role::AUTOMATION, correlation_id: SecureRandom.uuid)
13
+
14
+ Textus::Workflow::Scheduler.new(
15
+ registry: @registry,
16
+ workflow_queue: @container.store.workflow_queue,
17
+ reader: ->(key:) { @container.store_engine.read(key:) },
18
+ ).seed_expired
19
+
20
+ consumer = Textus::Workflow::Consumer.new(registry: @registry, container: @container, call:)
21
+ loop do
22
+ row = @container.store.workflow_queue.pop
23
+ break unless row
24
+
25
+ consumer.consume(row, @container.store.workflow_queue)
26
+ end
21
27
  end
22
28
 
23
29
  def run(poll: nil)
24
- interval = poll || @container.manifest.data.worker_config[:poll]
25
- lock = Textus::Port::WatcherLock.new(@container.root)
30
+ interval = poll || @container.manifest.data.worker_config[:poll] || 2
31
+ lock = Infra::Locks::WatcherLock.new(@container.root)
26
32
  lock.acquire
27
33
  begin
28
34
  loop do
@@ -0,0 +1,50 @@
1
+ require "sinatra/base"
2
+ require "json"
3
+
4
+ module Textus
5
+ module Surface
6
+ class Web < Sinatra::Base
7
+ set :textus_session, nil
8
+ set :port, 9292
9
+ set :bind, "0.0.0.0"
10
+ set :server, "puma"
11
+ set :show_exceptions, false
12
+
13
+ get "/api/status" do
14
+ content_type :json
15
+ s = settings.textus_session
16
+ boot = s.boot
17
+ JSON.generate(protocol: boot["protocol"], lanes: boot["lanes"]&.map { |l| l["name"] }, store_root: boot["store_root"])
18
+ end
19
+
20
+ post "/api/:verb" do
21
+ content_type :json
22
+ session = settings.textus_session.with_role(request.env["HTTP_X_TEXTUS_ROLE"] || "agent")
23
+ body = begin
24
+ JSON.parse(request.body.read)
25
+ rescue StandardError
26
+ {}
27
+ end
28
+ result = Textus::Surface::Dispatch.call(session, params[:verb], body)
29
+ JSON.generate(result)
30
+ rescue Textus::Error => e
31
+ halt status_for(e), JSON.generate(error: e.message, code: e.code.to_s)
32
+ end
33
+
34
+ not_found do
35
+ content_type :json
36
+ JSON.generate(error: "not found")
37
+ end
38
+
39
+ private
40
+
41
+ def status_for(err)
42
+ case err
43
+ when Textus::UnknownKey, Textus::UsageError then 404
44
+ when Textus::EtagMismatch then 409
45
+ else 422
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -10,7 +10,7 @@ module Textus
10
10
  new(
11
11
  role: role.to_s,
12
12
  correlation_id: correlation_id || SecureRandom.uuid,
13
- now: now || Textus::Port::Clock.new.now,
13
+ now: now || Time.now.utc,
14
14
  dry_run: dry_run,
15
15
  )
16
16
  end
@@ -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
 
@@ -16,8 +16,9 @@ module Textus
16
16
  # mid-session edit to any of these makes the cached orientation stale, so
17
17
  # the session must re-boot (ADR 0074). The composite is one digest over the
18
18
  # sorted per-file listing, so it is order-stable.
19
- def self.for_contract(root)
20
- listing = contract_files(root).map do |path|
19
+ def self.for_contract(layout)
20
+ root = layout.root
21
+ listing = contract_files(layout).map do |path|
21
22
  rel = path.delete_prefix(root).delete_prefix("/")
22
23
  "#{rel}:#{for_file(path)}"
23
24
  end.join("\n")
@@ -26,12 +27,11 @@ module Textus
26
27
 
27
28
  # manifest.yaml, then every hook and schema file. Dir.glob already returns
28
29
  # sorted paths (Ruby 3.0+), keeping the digest independent of FS order.
29
- def self.contract_files(root)
30
- geom = Textus::Store::Geometry.new(root)
30
+ def self.contract_files(layout)
31
31
  [
32
- geom.manifest_path,
33
- *Dir.glob(File.join(geom.hooks_dir, "**", "*.rb")),
34
- *Dir.glob(geom.schemas_glob).select { |f| File.file?(f) },
32
+ layout.manifest_path,
33
+ *Dir.glob(File.join(layout.hooks_dir, "**", "*.rb")),
34
+ *Dir.glob(layout.schemas_glob).select { |f| File.file?(f) },
35
35
  ]
36
36
  end
37
37
  end
@@ -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,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.55.1"
2
+ VERSION = "0.56.0"
3
3
  PROTOCOL = "textus/4"
4
4
  end