textus 0.55.2 → 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 (338) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +8 -9
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +0 -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/{dispatch → protocol}/binder.rb +1 -7
  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/{store → protocol}/freshness/ttl_evaluator.rb +7 -3
  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 → protocol}/layout.rb +16 -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 +22 -21
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +2 -7
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +7 -7
  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 +12 -12
  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/server.rb +35 -24
  141. data/lib/textus/surface/watcher.rb +18 -12
  142. data/lib/textus/surface/web.rb +50 -0
  143. data/lib/textus/value/call.rb +1 -1
  144. data/lib/textus/value/etag.rb +7 -7
  145. data/lib/textus/version.rb +1 -1
  146. data/lib/textus/workflow/consumer.rb +39 -0
  147. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  148. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  149. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  150. data/lib/textus/workflow/dsl.rb +111 -21
  151. data/lib/textus/workflow/event_emitter.rb +21 -0
  152. data/lib/textus/workflow/publisher.rb +59 -0
  153. data/lib/textus/workflow/registry.rb +31 -8
  154. data/lib/textus/workflow/runner.rb +25 -53
  155. data/lib/textus/workflow/scheduler.rb +45 -0
  156. data/lib/textus/workflow/step_executor.rb +78 -0
  157. data/lib/textus/workflow/step_helpers.rb +102 -0
  158. data/lib/textus/workflow.rb +2 -5
  159. data/lib/textus.rb +16 -16
  160. metadata +169 -185
  161. data/SPEC.md +0 -909
  162. data/docs/architecture/README.md +0 -273
  163. data/docs/reference/conventions.md +0 -141
  164. data/lib/textus/boot.rb +0 -181
  165. data/lib/textus/dispatch/contracts.rb +0 -63
  166. data/lib/textus/dispatch/handler_registry.rb +0 -21
  167. data/lib/textus/dispatch/middleware/audit_index.rb +0 -51
  168. data/lib/textus/dispatch/middleware/auth.rb +0 -40
  169. data/lib/textus/dispatch/middleware/base.rb +0 -26
  170. data/lib/textus/dispatch/middleware/binder.rb +0 -20
  171. data/lib/textus/dispatch/middleware/cascade.rb +0 -53
  172. data/lib/textus/dispatch/pipeline.rb +0 -35
  173. data/lib/textus/doctor/check/audit_log.rb +0 -34
  174. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  175. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  176. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  177. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  178. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  179. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  180. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  181. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  182. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  183. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  184. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  185. data/lib/textus/doctor/check/schemas.rb +0 -26
  186. data/lib/textus/doctor/check/scratchpad_sources.rb +0 -55
  187. data/lib/textus/doctor/check/sentinels.rb +0 -60
  188. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  189. data/lib/textus/doctor/check/templates.rb +0 -29
  190. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  191. data/lib/textus/doctor/check.rb +0 -43
  192. data/lib/textus/doctor/validator.rb +0 -93
  193. data/lib/textus/doctor.rb +0 -54
  194. data/lib/textus/format/base.rb +0 -77
  195. data/lib/textus/format/json.rb +0 -81
  196. data/lib/textus/format/markdown.rb +0 -59
  197. data/lib/textus/format/text.rb +0 -55
  198. data/lib/textus/format/yaml.rb +0 -104
  199. data/lib/textus/format.rb +0 -61
  200. data/lib/textus/handlers/maintenance/boot_store.rb +0 -15
  201. data/lib/textus/handlers/maintenance/doctor_store.rb +0 -15
  202. data/lib/textus/handlers/maintenance/drain_store.rb +0 -21
  203. data/lib/textus/handlers/maintenance/ingest_entry.rb +0 -159
  204. data/lib/textus/handlers/maintenance/jobs_action.rb +0 -21
  205. data/lib/textus/handlers/maintenance/published_entries.rb +0 -17
  206. data/lib/textus/handlers/maintenance/rule_explain.rb +0 -77
  207. data/lib/textus/handlers/maintenance/rule_lint.rb +0 -54
  208. data/lib/textus/handlers/maintenance/rule_list.rb +0 -32
  209. data/lib/textus/handlers/maintenance/schema_envelope.rb +0 -19
  210. data/lib/textus/handlers/read/audit_entries.rb +0 -48
  211. data/lib/textus/handlers/read/blame_entry.rb +0 -71
  212. data/lib/textus/handlers/read/deps_entry.rb +0 -17
  213. data/lib/textus/handlers/read/get_entry.rb +0 -68
  214. data/lib/textus/handlers/read/list_keys.rb +0 -36
  215. data/lib/textus/handlers/read/pulse_entries.rb +0 -66
  216. data/lib/textus/handlers/read/rdeps_entry.rb +0 -21
  217. data/lib/textus/handlers/read/uid_entry.rb +0 -18
  218. data/lib/textus/handlers/read/where_entry.rb +0 -18
  219. data/lib/textus/handlers/write/accept_proposal.rb +0 -39
  220. data/lib/textus/handlers/write/data_mv.rb +0 -55
  221. data/lib/textus/handlers/write/delete_key.rb +0 -17
  222. data/lib/textus/handlers/write/enqueue_job.rb +0 -27
  223. data/lib/textus/handlers/write/key_delete_prefix.rb +0 -32
  224. data/lib/textus/handlers/write/key_mv_prefix.rb +0 -45
  225. data/lib/textus/handlers/write/move_key.rb +0 -80
  226. data/lib/textus/handlers/write/propose_entry.rb +0 -29
  227. data/lib/textus/handlers/write/put_entry.rb +0 -29
  228. data/lib/textus/handlers/write/reject_proposal.rb +0 -29
  229. data/lib/textus/init.rb +0 -157
  230. data/lib/textus/jobs.rb +0 -9
  231. data/lib/textus/key/distance.rb +0 -55
  232. data/lib/textus/key/grammar.rb +0 -33
  233. data/lib/textus/key/matching.rb +0 -24
  234. data/lib/textus/key/path.rb +0 -28
  235. data/lib/textus/manifest/capabilities.rb +0 -29
  236. data/lib/textus/manifest/data.rb +0 -102
  237. data/lib/textus/manifest/entry/base.rb +0 -105
  238. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  239. data/lib/textus/manifest/entry/leaf.rb +0 -17
  240. data/lib/textus/manifest/entry/nested.rb +0 -37
  241. data/lib/textus/manifest/entry/parser.rb +0 -99
  242. data/lib/textus/manifest/entry/produced.rb +0 -36
  243. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  244. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  245. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  246. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  247. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  248. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  249. data/lib/textus/manifest/entry/publish.rb +0 -45
  250. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  251. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  252. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  253. data/lib/textus/manifest/entry/validators.rb +0 -18
  254. data/lib/textus/manifest/entry.rb +0 -8
  255. data/lib/textus/manifest/policy/matcher.rb +0 -51
  256. data/lib/textus/manifest/policy/predicates/author_held.rb +0 -22
  257. data/lib/textus/manifest/policy/predicates/etag_match.rb +0 -18
  258. data/lib/textus/manifest/policy/predicates/fresh_within.rb +0 -13
  259. data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +0 -31
  260. data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +0 -23
  261. data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +0 -25
  262. data/lib/textus/manifest/policy/predicates/raw_write_once.rb +0 -24
  263. data/lib/textus/manifest/policy/predicates/schema_valid.rb +0 -41
  264. data/lib/textus/manifest/policy/predicates/target_is_canon.rb +0 -20
  265. data/lib/textus/manifest/policy/predicates.rb +0 -54
  266. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  267. data/lib/textus/manifest/policy/react.rb +0 -30
  268. data/lib/textus/manifest/policy/retention.rb +0 -29
  269. data/lib/textus/manifest/policy/source.rb +0 -30
  270. data/lib/textus/manifest/policy.rb +0 -95
  271. data/lib/textus/manifest/resolver.rb +0 -118
  272. data/lib/textus/manifest/rules.rb +0 -86
  273. data/lib/textus/manifest/schema/contract.rb +0 -61
  274. data/lib/textus/manifest/schema/keys.rb +0 -90
  275. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  276. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  277. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  278. data/lib/textus/manifest/schema/semantics.rb +0 -27
  279. data/lib/textus/manifest/schema/validator.rb +0 -48
  280. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  281. data/lib/textus/manifest/schema.rb +0 -30
  282. data/lib/textus/manifest.rb +0 -65
  283. data/lib/textus/orchestration.rb +0 -55
  284. data/lib/textus/port/audit_log.rb +0 -268
  285. data/lib/textus/port/build_lock.rb +0 -64
  286. data/lib/textus/port/clock.rb +0 -10
  287. data/lib/textus/port/publisher.rb +0 -60
  288. data/lib/textus/port/sentinel.rb +0 -25
  289. data/lib/textus/port/sentinel_store.rb +0 -83
  290. data/lib/textus/port/storage/file_stat.rb +0 -19
  291. data/lib/textus/port/storage/file_store.rb +0 -49
  292. data/lib/textus/port/storage/interface.rb +0 -17
  293. data/lib/textus/port/store.rb +0 -149
  294. data/lib/textus/port/watcher_lock.rb +0 -48
  295. data/lib/textus/produce/engine.rb +0 -40
  296. data/lib/textus/produce/publisher.rb +0 -21
  297. data/lib/textus/produce/render.rb +0 -20
  298. data/lib/textus/schema/registry.rb +0 -42
  299. data/lib/textus/schema/tools.rb +0 -108
  300. data/lib/textus/schema.rb +0 -100
  301. data/lib/textus/store/container.rb +0 -173
  302. data/lib/textus/store/cursor.rb +0 -26
  303. data/lib/textus/store/entry/reader.rb +0 -47
  304. data/lib/textus/store/entry/writer.rb +0 -219
  305. data/lib/textus/store/envelope/meta.rb +0 -61
  306. data/lib/textus/store/freshness/drift_detector.rb +0 -93
  307. data/lib/textus/store/freshness/evaluator.rb +0 -20
  308. data/lib/textus/store/freshness/verdict.rb +0 -24
  309. data/lib/textus/store/freshness.rb +0 -8
  310. data/lib/textus/store/index/builder.rb +0 -76
  311. data/lib/textus/store/index/lookup.rb +0 -60
  312. data/lib/textus/store/jobs/base.rb +0 -13
  313. data/lib/textus/store/jobs/index.rb +0 -15
  314. data/lib/textus/store/jobs/materialize.rb +0 -15
  315. data/lib/textus/store/jobs/plan.rb +0 -11
  316. data/lib/textus/store/jobs/planner.rb +0 -124
  317. data/lib/textus/store/jobs/queue.rb +0 -162
  318. data/lib/textus/store/jobs/registry.rb +0 -19
  319. data/lib/textus/store/jobs/retention/base.rb +0 -52
  320. data/lib/textus/store/jobs/retention/sweep.rb +0 -55
  321. data/lib/textus/store/jobs/retention.rb +0 -8
  322. data/lib/textus/store/jobs/sweep.rb +0 -21
  323. data/lib/textus/store/jobs/worker.rb +0 -64
  324. data/lib/textus/store.rb +0 -122
  325. data/lib/textus/surface/cli/verb/doctor.rb +0 -20
  326. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  327. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  328. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  329. data/lib/textus/surface/mcp/projector.rb +0 -20
  330. data/lib/textus/surface/mcp.rb +0 -8
  331. data/lib/textus/value/command.rb +0 -16
  332. data/lib/textus/value/envelope.rb +0 -90
  333. data/lib/textus/value/result.rb +0 -46
  334. data/lib/textus/verb_registry.rb +0 -417
  335. data/lib/textus/workflow/collector.rb +0 -27
  336. data/lib/textus/workflow/context.rb +0 -5
  337. data/lib/textus/workflow/loader.rb +0 -17
  338. data/lib/textus/workflow/pattern.rb +0 -18
@@ -1,21 +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
11
  @store = store.with_role(role)
15
12
  @stdin = stdin
16
13
  @stdout = stdout
14
+ @shutdown = false
15
+ @cursor_manager = CursorManager.new(store: @store, role:)
17
16
 
18
- @sdk = ::MCP::Server.new(
17
+ @store = @cursor_manager.resume! || @store
18
+
19
+ @sdk = MCP_ADAPTER.server(
19
20
  name: "textus",
20
21
  version: Textus::VERSION,
21
22
  tools: Catalog.build_tools(self),
@@ -40,13 +41,17 @@ module Textus
40
41
 
41
42
  def dispatch(verb_name, args, _server_context)
42
43
  str_args = deep_stringify_keys(args)
43
- @store.check_etag!(contract_etag_now) unless Catalog.read_verbs.include?(verb_name.to_s)
44
+ drifted = drift_detected?
45
+
44
46
  result = Catalog.call(verb_name.to_s, store: @store, args: str_args)
45
- @store = @store.advance_cursor(@store.audit_log.latest_seq) if verb_name == :pulse
46
- @store = @store.with_role(@store.role) if verb_name == :boot
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)
47
54
  ::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
48
- rescue Textus::ContractDrift => e
49
- raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
50
55
  rescue Textus::CursorExpired => e
51
56
  raise_handler_error(e.message, Textus::CursorExpired::JSONRPC_CODE)
52
57
  rescue Textus::Surface::MCP::ToolError => e
@@ -57,23 +62,29 @@ module Textus
57
62
 
58
63
  private
59
64
 
60
- # Snapshot at server init against the boot-time manifest. New produced entries
61
- # added by a later reconcile are invisible until the server restarts — this is
62
- # intentional: a ContractDrift will gate writes on any mid-session manifest change.
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
72
+ end
73
+
63
74
  def build_resources
64
- machine_lane = @store.manifest.policy.machine_lane
75
+ machine_lane = @store.respond_to?(:container) ? @store.container.manifest.policy.machine_lane : nil
65
76
  return [] unless machine_lane
66
77
 
67
- @store.manifest.data.entries
68
- .select { |e| e.lane == machine_lane && e.is_a?(Textus::Manifest::Entry::Produced) }
69
- .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)) }
70
81
  end
71
82
 
72
83
  def handle_resource_read(uri, _server_context)
73
84
  key = uri.delete_prefix("textus://").tr("/", ".")
74
85
  env = @store.get(key:)
75
- text = env.content.is_a?(Hash) ? JSON.dump(env.content) : (env.body || "").to_s
76
- mime = mime_for_format(env.format)
86
+ text = env["content"].is_a?(Hash) ? JSON.dump(env["content"]) : (env["body"] || "").to_s
87
+ mime = mime_for_format(env["format"])
77
88
  [{ uri: uri, mimeType: mime, text: text }]
78
89
  rescue Textus::Error => e
79
90
  raise_handler_error("resource read failed: #{e.message}", -32_603)
@@ -81,10 +92,10 @@ module Textus
81
92
  raise_handler_error("internal: #{e.message}", -32_603)
82
93
  end
83
94
 
84
- 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]
85
98
 
86
- # The SDK parses JSON with symbolize_names:true, making all nested hash keys symbols.
87
- # Recursively stringify so Catalog.call receives string-keyed hashes throughout.
88
99
  def deep_stringify_keys(obj)
89
100
  case obj
90
101
  when Hash then obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
@@ -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
@@ -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::Layout.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
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.55.2"
2
+ VERSION = "0.56.0"
3
3
  PROTOCOL = "textus/4"
4
4
  end
@@ -0,0 +1,39 @@
1
+ module Textus
2
+ module Workflow
3
+ class Consumer
4
+ BACKOFF_DELAYS = [10, 30, 60].freeze
5
+
6
+ def initialize(registry:, container:, call:)
7
+ @registry = registry
8
+ @container = container
9
+ @call = call
10
+ end
11
+
12
+ def consume(row, workflow_queue)
13
+ workflow_queue.mark_processing(row[:id])
14
+ workflow = @registry.for(row[:workflow_type])
15
+ Runner.new(workflow, container: @container, call: @call).run(row[:key])
16
+ workflow_queue.complete(row[:id])
17
+ row.merge(state: "done")
18
+ rescue StandardError => e
19
+ handle_failure(row, workflow_queue, e)
20
+ end
21
+
22
+ private
23
+
24
+ def handle_failure(row, workflow_queue, error)
25
+ retry_count = (row[:retry_count] || 0) + 1
26
+ max_attempts = row[:max_attempts] || 3
27
+
28
+ if retry_count >= max_attempts
29
+ workflow_queue.fail(row[:id], retry_count:, error: error.message)
30
+ row.merge(state: "failed", error: error.message)
31
+ else
32
+ index = [retry_count - 1, BACKOFF_DELAYS.length - 1].min
33
+ workflow_queue.reschedule(row[:id], retry_count:, delay: BACKOFF_DELAYS[index], error: error.message)
34
+ row.merge(state: "retrying")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ module Textus
2
+ module Workflow
3
+ module DSL
4
+ module Lifecycle
5
+ VALID_EXPIRE_ACTIONS = %i[drop archive].freeze
6
+
7
+ def ttl_value(cadence)
8
+ return nil if cadence == :__no_arg__
9
+
10
+ Textus::Value::Duration.seconds(cadence)
11
+ end
12
+
13
+ def expire_action(action = :__no_arg__)
14
+ return nil if action == :__no_arg__
15
+
16
+ sym = action.to_s.to_sym
17
+ raise ArgumentError.new("on_expire must be one of drop|archive, got #{action.inspect}") unless VALID_EXPIRE_ACTIONS.include?(sym)
18
+
19
+ sym
20
+ end
21
+
22
+ def max_attempts_value(n)
23
+ n.to_i
24
+ end
25
+
26
+ def priority_value(n)
27
+ n.to_i
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ module Textus
2
+ module Workflow
3
+ module DSL
4
+ module PatternMatcher
5
+ def match_pattern?(pattern, key)
6
+ if pattern.end_with?(".**")
7
+ prefix = pattern.delete_suffix(".**")
8
+ key.start_with?("#{prefix}.")
9
+ elsif pattern.end_with?(".*")
10
+ prefix = pattern.delete_suffix(".*")
11
+ suffix = key.delete_prefix("#{prefix}.")
12
+ key != suffix && !suffix.include?(".")
13
+ else
14
+ key == pattern
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ module Textus
2
+ module Workflow
3
+ module DSL
4
+ module StepBuilder
5
+ def build_step(name, callable = nil, timeout: nil, &block)
6
+ if callable.is_a?(Hash)
7
+ raise ArgumentError.new(
8
+ "step :#{name} no longer accepts options hash; pass timeout: keyword instead",
9
+ )
10
+ end
11
+
12
+ resolved = if callable.respond_to?(:call)
13
+ callable
14
+ elsif block
15
+ block
16
+ else
17
+ raise ArgumentError.new("step :#{name} requires a block or a callable (got neither)")
18
+ end
19
+
20
+ Step.new(name: name, callable: resolved, timeout: timeout)
21
+ end
22
+
23
+ def build_check(name, naming: nil, scope: nil, &block)
24
+ merge = lambda { |data, new_issues|
25
+ existing = Array(data&.dig("content", "issues"))
26
+ all = existing + new_issues
27
+ { "content" => { "ok" => all.empty?, "issues" => all, "count" => all.size } }
28
+ }
29
+
30
+ if naming
31
+ raise ArgumentError.new("check :#{name} requires a scope: for naming checks") unless scope
32
+
33
+ lambda { |data, ctx|
34
+ entries = ctx.container.manifest.resolver.enumerate.select do |row|
35
+ match_pattern?(scope, row[:key].to_s)
36
+ end
37
+ new_issues = check_naming(
38
+ entries, pattern: naming, code: "check.#{name}",
39
+ fix: "rename to match #{naming.inspect}"
40
+ )
41
+ merge.call(data, new_issues)
42
+ }
43
+ elsif block
44
+ ->(data, ctx) { merge.call(data, block.call(ctx) || []) }
45
+ else
46
+ raise ArgumentError.new("check :#{name} requires naming: or a block")
47
+ end
48
+ end
49
+
50
+ def build_parallel(steps)
51
+ Parallel.new(steps: steps)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,41 +2,131 @@ module Textus
2
2
  module Workflow
3
3
  module DSL
4
4
  Step = Data.define(:name, :callable, :timeout)
5
+ Parallel = Data.define(:steps)
6
+ PublishTarget = Data.define(:to, :template, :tree, :inject_boot)
5
7
 
6
8
  class Definition
7
- attr_reader :name, :steps, :match_pattern, :publish_block
9
+ include StepBuilder
10
+ include PatternMatcher
11
+ include Lifecycle
12
+
13
+ attr_reader :name, :steps, :on_patterns, :save_to, :notify_keys,
14
+ :publish_block, :publish_targets,
15
+ :handled_events, :workflow_type
16
+
17
+ def self.register(defn)
18
+ @definitions ||= []
19
+ @definitions << defn
20
+ end
21
+
22
+ def self.definitions
23
+ @definitions || []
24
+ end
25
+
26
+ def self.clear_registered
27
+ @definitions = []
28
+ end
8
29
 
9
30
  def initialize(name)
10
- @name = name
11
- @steps = []
12
- @match_pattern = nil
13
- @publish_block = nil
31
+ @name = name
32
+ @steps = []
33
+ @on_patterns = []
34
+ @save_to = nil
35
+ @notify_keys = []
36
+ @publish_block = nil
37
+ @publish_targets = []
38
+ @priority = 0
39
+ @handled_events = []
40
+ @max_attempts = 3
41
+ @workflow_type = name
14
42
  end
15
43
 
16
- def match(pattern)
17
- @match_pattern = pattern
44
+ # ── Event handling (internal) ──
45
+ def handles(*event_types)
46
+ @handled_events.concat(event_types.map(&:to_s).freeze)
18
47
  end
19
48
 
20
- def step(name, callable_or_opt = nil, timeout: nil, &block)
21
- callable = if callable_or_opt.respond_to?(:call)
22
- callable_or_opt
23
- elsif block
24
- block
25
- else
26
- raise ArgumentError.new("step :#{name} requires a block or a callable (got neither)")
27
- end
28
- t = callable_or_opt.is_a?(Hash) ? callable_or_opt[:timeout] : timeout
29
- @steps << Step.new(name: name, callable: callable, timeout: t)
49
+ def type
50
+ @workflow_type
30
51
  end
31
52
 
32
- def publish(&block)
33
- @publish_block = block || :default
53
+ def max_attempts(n = nil)
54
+ return @max_attempts if n.nil?
55
+
56
+ @max_attempts = max_attempts_value(n)
34
57
  end
35
58
 
59
+ # ── Trigger ──
60
+ def on(*patterns)
61
+ @on_patterns = patterns
62
+ end
63
+
64
+ # ── Steps ──
65
+ def step(name, callable = nil, timeout: nil, &)
66
+ @steps << build_step(name, callable, timeout:, &)
67
+ end
68
+
69
+ def check(name, naming: nil, scope: nil, &)
70
+ callable = build_check(name, naming:, scope:, &)
71
+ @steps << Step.new(name: :"check_#{name}", callable: callable, timeout: nil)
72
+ end
73
+
74
+ def parallel(&)
75
+ saved = @steps
76
+ @steps = []
77
+ instance_eval(&)
78
+ parallel_steps = @steps.dup
79
+ @steps = saved << build_parallel(parallel_steps)
80
+ end
81
+
82
+ # ── Output ──
83
+ def publish(to: nil, template: nil, tree: nil, inject_boot: nil, &block)
84
+ if block
85
+ @publish_block = block
86
+ elsif to || tree
87
+ @publish_targets << PublishTarget.new(to: to, template: template, tree: tree, inject_boot: inject_boot)
88
+ else
89
+ raise ArgumentError.new("publish requires to: (file), tree:, or a block; use save to: for store keys")
90
+ end
91
+ end
92
+
93
+ def save(to:)
94
+ @save_to = to
95
+ end
96
+
97
+ def notify(key)
98
+ @notify_keys << key
99
+ end
100
+
101
+ # ── Ordering ──
102
+ def priority(n = nil)
103
+ return @priority if n.nil?
104
+
105
+ @priority = priority_value(n)
106
+ end
107
+
108
+ # ── Lifecycle ──
109
+ def every(cadence = :__no_arg__)
110
+ return @ttl if cadence == :__no_arg__
111
+
112
+ @ttl = ttl_value(cadence)
113
+ end
114
+
115
+ alias ttl every
116
+
117
+ def on_expire(action = :__no_arg__)
118
+ @on_expire = expire_action(action)
119
+ end
120
+
121
+ # ── Matching (internal) ──
36
122
  def match?(key)
37
- return false unless @match_pattern
123
+ return false unless @on_patterns&.any?
124
+
125
+ @on_patterns.any? { |p| match_pattern?(p, key) }
126
+ end
38
127
 
39
- Pattern.match?(@match_pattern, key)
128
+ def multi_match?
129
+ @on_patterns&.any? { |p| p.end_with?("**") }
40
130
  end
41
131
  end
42
132
  end
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ module Workflow
3
+ class EventEmitter
4
+ def initialize(workflow_queue, registry:)
5
+ @workflow_queue = workflow_queue
6
+ @registry = registry
7
+ end
8
+
9
+ def emit(event_type:, key:, lane: "artifacts")
10
+ workflows = @registry.workflows_for(event_type)
11
+ workflows.each do |wf|
12
+ @workflow_queue.push(
13
+ event_type: event_type.to_s, workflow_type: wf.type,
14
+ key:, lane:, max_attempts: wf.max_attempts
15
+ )
16
+ end
17
+ workflows.size
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ module Textus
2
+ module Workflow
3
+ class Publisher
4
+ def initialize(container:, call:)
5
+ @container = container
6
+ @call = call
7
+ @store_engine = container.store_engine
8
+ end
9
+
10
+ def publish(key, data, ctx, definition)
11
+ definition.publish_targets.each { |t| publish_one(key, data, ctx, target: t) }
12
+
13
+ write_to_key(definition.save_to, data, ctx) if definition.save_to
14
+
15
+ definition.notify_keys.each { |nk| notify_key(nk, ctx) }
16
+
17
+ definition.publish_block&.call(data, ctx)
18
+ end
19
+
20
+ private
21
+
22
+ def write_to_key(target_key, data, ctx) # rubocop:disable Lint/UnusedMethodArgument
23
+ res = @container.manifest.resolver.resolve(target_key)
24
+ normalized = Textus::Protocol::Format.data_to_payload(data, res.entry.format)
25
+ meta = stamped_meta(normalized[:meta])
26
+ @store_engine.put(key: target_key, meta:, body: normalized[:body], content: normalized[:content], call: @call)
27
+ end
28
+
29
+ def notify_key(target_key, ctx) # rubocop:disable Lint/UnusedMethodArgument
30
+ @store_engine.put(key: target_key, meta: {}, body: nil, content: {}, call: @call)
31
+ end
32
+
33
+ def write_and_pctx(key, data, ctx)
34
+ normalized = Textus::Protocol::Format.data_to_payload(data, ctx.entry.format)
35
+ meta = stamped_meta(normalized[:meta])
36
+ @store_engine.put(key:, meta:, body: normalized[:body], content: normalized[:content], call: @call)
37
+ Textus::Protocol::Manifest::Entry::Base::PublishContext.new(
38
+ container: @container,
39
+ reader: ->(key:) { @store_engine.read(key:) },
40
+ )
41
+ end
42
+
43
+ def publish_one(key, data, ctx, target:)
44
+ pctx = write_and_pctx(key, data, ctx)
45
+ attrs = { "to" => target.to, "template" => target.template,
46
+ "tree" => target.tree }
47
+ attrs["inject_boot"] = true if target.inject_boot
48
+ pub_target = Textus::Protocol::Manifest::Policy::PublishTarget.new(attrs)
49
+ entry = ctx.entry.with_publish_targets([pub_target])
50
+ entry.publish_via(pctx)
51
+ end
52
+
53
+ def stamped_meta(meta)
54
+ now = Time.now.utc.iso8601
55
+ (meta || {}).merge("generated_at" => now)
56
+ end
57
+ end
58
+ end
59
+ end