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
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textus
4
+ module Protocol
5
+ module VerbRegistry
6
+ CoreVerbs = {
7
+ audit: VerbSpec.new(
8
+ :audit, "Query the audit log with optional filters.",
9
+ [ArgSpec.arg(name: :key, description: "filter to rows for this key"),
10
+ ArgSpec.arg(name: :lane, description: "filter to keys in this lane"),
11
+ ArgSpec.arg(name: :role, description: "filter to rows written under this role"),
12
+ ArgSpec.arg(name: :verb, description: "filter to rows for this verb"),
13
+ ArgSpec.arg(name: :since, description: "ISO-8601 timestamp or relative offset (e.g. 1h, 30m)"),
14
+ ArgSpec.arg(name: :seq_since, type: Integer, description: "return rows with seq > this cursor value"),
15
+ ArgSpec.arg(name: :correlation_id, description: "filter to rows with this correlation_id"),
16
+ ArgSpec.arg(name: :limit, type: Integer, description: "maximum number of rows to return")],
17
+ %i[cli mcp], :read
18
+ ),
19
+
20
+ blame: VerbSpec.new(
21
+ :blame, "Annotate audit rows with the git commit that introduced each file state.",
22
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key to blame"),
23
+ ArgSpec.arg(name: :limit, type: Integer, description: "maximum number of audit rows to return")],
24
+ %i[cli mcp], :read
25
+ ),
26
+
27
+ boot: VerbSpec.new(
28
+ :boot, "Return the orientation contract: lanes, agent_quickstart, agent_protocol.",
29
+ [], %i[cli mcp], :read
30
+ ),
31
+
32
+ data_mv: VerbSpec.new(
33
+ :data_mv, "Rename a data lane — manifest + files. Refuses if destination exists.",
34
+ [ArgSpec.arg(name: :from, required: true, positional: true, description: "current data lane name"),
35
+ ArgSpec.arg(name: :to, required: true, positional: true, description: "new data lane name"),
36
+ ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
37
+ description: "when true, returns planned zone move without applying")],
38
+ %i[cli mcp], :write
39
+ ),
40
+
41
+ deps: VerbSpec.new(
42
+ :deps, "List the keys a derived entry depends on.",
43
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key of the derived entry whose source keys you want")],
44
+ %i[cli mcp], :read
45
+ ),
46
+
47
+ drain: VerbSpec.new(
48
+ :drain, "Seed materialize + sweep jobs then drain the queue to empty.",
49
+ [], %i[cli mcp], :maintenance
50
+ ),
51
+
52
+ get: VerbSpec.new(
53
+ :get, "Read one entry — on-disk read with freshness verdict.",
54
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key to read, e.g. 'knowledge.project'")],
55
+ %i[cli mcp], :read
56
+ ),
57
+
58
+ graph: VerbSpec.new(
59
+ :graph, "Query the link graph — neighbors (direct links) or reachable (transitive, with optional depth limit).",
60
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key to query"),
61
+ ArgSpec.arg(name: :depth, type: Integer, description: "max traversal depth for reachable query (unlimited when omitted)")],
62
+ %i[cli mcp], :read
63
+ ),
64
+
65
+ ingest: VerbSpec.new(
66
+ verb: :ingest, summary: "Capture external source material into the raw lane. Write-once.",
67
+ args: [ArgSpec.arg(name: :kind, required: true, positional: true, description: "source kind: url | file | asset"),
68
+ ArgSpec.arg(name: :slug, required: true, description: "human slug for the key suffix (kebab-case)"),
69
+ ArgSpec.arg(name: :url, description: "remote URL (required when kind=url)"),
70
+ ArgSpec.arg(name: :path, description: "local file path (required when kind=file or kind=asset)"),
71
+ ArgSpec.arg(name: :lane, description: "asset group subdirectory (required when kind=asset)"),
72
+ ArgSpec.arg(name: :label, description: "human label stored in source.label")],
73
+ surfaces: %i[cli mcp], category: :write, lane: :ingest
74
+ ),
75
+
76
+ key_delete: VerbSpec.new(
77
+ :key_delete, "Delete one entry by key. Returns {ok, key, deleted}.",
78
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key to delete"),
79
+ ArgSpec.arg(name: :if_etag, description: "optimistic-concurrency guard: etag you last read")],
80
+ %i[cli mcp], :write
81
+ ),
82
+
83
+ key_delete_prefix: VerbSpec.new(
84
+ :key_delete_prefix, "Bulk-delete every leaf key under prefix.",
85
+ [ArgSpec.arg(name: :prefix, required: true, positional: true, description: "every leaf key under this dotted prefix is deleted"),
86
+ ArgSpec.arg(name: :dry_run, type: :boolean, default: false,
87
+ description: "when true, returns keys that would be deleted without deleting")],
88
+ %i[cli mcp], :write
89
+ ),
90
+
91
+ key_mv: VerbSpec.new(
92
+ :key_mv, "Rename one entry (same zone + format). Refuses if target exists.",
93
+ [ArgSpec.arg(name: :old_key, required: true, positional: true, description: "current dotted key"),
94
+ ArgSpec.arg(name: :new_key, required: true, positional: true, description: "new dotted key (same zone and format)"),
95
+ ArgSpec.arg(name: :dry_run, type: :boolean, description: "when true, returns planned move without applying")],
96
+ %i[cli mcp], :write
97
+ ),
98
+
99
+ key_mv_prefix: VerbSpec.new(
100
+ :key_mv_prefix, "Bulk-rename every leaf key under from_prefix to to_prefix.",
101
+ [ArgSpec.arg(name: :from_prefix, required: true, positional: true, description: "dotted prefix whose leaf keys are renamed"),
102
+ ArgSpec.arg(name: :to_prefix, required: true, positional: true, description: "dotted prefix the keys are renamed to"),
103
+ ArgSpec.arg(name: :dry_run, type: :boolean, default: false, description: "when true, returns planned moves without applying")],
104
+ %i[cli mcp], :write
105
+ ),
106
+
107
+ list: VerbSpec.new(
108
+ :list, "List keys filtered by lane and/or prefix.",
109
+ [ArgSpec.arg(name: :prefix, description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.execution'"),
110
+ ArgSpec.arg(name: :lane, description: "restrict to one lane by name (see `boot` lanes)"),
111
+ ArgSpec.arg(name: :q, description: "full-text search query over entry content (FTS5)"),
112
+ ArgSpec.arg(name: :schema, description: "filter to entries whose schema matches this name")],
113
+ %i[cli mcp], :read
114
+ ),
115
+
116
+ pulse: VerbSpec.new(
117
+ :pulse, "Delta since cursor — changed entries, pending proposals, index freshness.",
118
+ [ArgSpec.arg(name: :since, type: Integer, description: "audit seq to diff from; defaults to the session cursor")],
119
+ %i[cli mcp], :read
120
+ ),
121
+
122
+ put: VerbSpec.new(
123
+ :put, "Create or update an entry. Schema-validated. Returns {uid, etag}.",
124
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"),
125
+ ArgSpec.arg(name: :meta, type: Hash, wire_name: :_meta,
126
+ description: "frontmatter; reads back as `_meta`. Schema-validated — call `schema KEY` first"),
127
+ ArgSpec.arg(name: :data, description: "unified payload — String for body (markdown/text), Hash for content (json/yaml)"),
128
+ ArgSpec.arg(name: :if_etag, description: "optimistic-concurrency guard; write rejected if entry changed since"),
129
+ ArgSpec.arg(name: :format, description: "override entry format for this write (e.g. script)")],
130
+ %i[cli mcp], :write
131
+ ),
132
+
133
+ rdeps: VerbSpec.new(
134
+ :rdeps, "List the derived entries that depend on a key (reverse deps).",
135
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key whose dependents you want")],
136
+ %i[cli mcp], :read
137
+ ),
138
+
139
+ schema_list: VerbSpec.new(
140
+ :schema_list, "List all registered schemas with their fields.",
141
+ [], %i[cli mcp], :read
142
+ ),
143
+
144
+ schema_show: VerbSpec.new(
145
+ :schema_show, "Return the schema (field shape) for an entry's family.",
146
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "any key in the family whose schema you want")],
147
+ %i[cli mcp], :read
148
+ ),
149
+
150
+ uid: VerbSpec.new(
151
+ :uid, "Return the stable UID of an entry without reading its body.",
152
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "entry key")],
153
+ %i[cli mcp], :read
154
+ ),
155
+
156
+ where: VerbSpec.new(
157
+ :where, "Resolve a key to its zone, owner, and path without reading the body.",
158
+ [ArgSpec.arg(name: :key, required: true, positional: true, description: "dotted key to locate")],
159
+ %i[cli mcp], :read
160
+ ),
161
+ }.freeze
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,42 @@
1
+ module Textus
2
+ module Protocol
3
+ module VerbRegistry
4
+ TYPE_MAP = {
5
+ String => "string", Integer => "integer", Hash => "object",
6
+ Array => "array", :boolean => "boolean"
7
+ }.freeze
8
+
9
+ SYSTEM_VERBS = %i[boot audit blame pulse drain workflow_spec].to_set.freeze
10
+
11
+ VerbSpec = Data.define(:verb, :summary, :args, :surfaces, :category) do
12
+ def initialize(verb:, summary:, args:, surfaces:, category:, lane: nil)
13
+ @lane = lane
14
+ super(verb:, summary:, args:, surfaces:, category:)
15
+ end
16
+
17
+ def lane = @lane
18
+ def system? = SYSTEM_VERBS.include?(verb)
19
+ def mcp? = surfaces.include?(:mcp)
20
+ def cli? = surfaces.include?(:cli)
21
+ def cli_path = verb.to_s.tr("_", " ")
22
+ def cli_words = cli_path.split
23
+ def cli_group = cli_words.size > 1 ? cli_words.first : nil
24
+ def cli_leaf = cli_words.last
25
+ def required_args = args.select(&:required)
26
+ def read? = category == :read
27
+ def write? = category == :write
28
+ def maintenance? = category == :maintenance
29
+
30
+ def input_schema
31
+ props = args.to_h do |a|
32
+ json_type = VerbRegistry::TYPE_MAP[a.type] || "string"
33
+ h = { "type" => json_type }
34
+ h["description"] = a.description if a.description
35
+ [a.wire.to_s, h]
36
+ end
37
+ { type: "object", properties: props, required: required_args.map { |a| a.wire.to_s } }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module Textus
2
+ module Protocol
3
+ module VerbRegistry
4
+ require_relative "verb_registry/arg_spec"
5
+ require_relative "verb_registry/verb_spec"
6
+
7
+ VERBS = {}
8
+ POSITIONAL = {}
9
+
10
+ def self.register(spec)
11
+ VERBS[spec.verb] = spec
12
+ POSITIONAL[spec.verb] = spec.args.select(&:positional).map(&:name)
13
+ end
14
+
15
+ def self.for(verb) = VERBS[verb]
16
+ def self.positional_for(verb) = POSITIONAL[verb] || []
17
+ def self.summary_for(verb) = VERBS[verb]&.summary
18
+ def self.registered = VERBS.values
19
+
20
+ ENTRY_VERBS = %i[
21
+ get put list key_delete key_mv propose accept reject
22
+ audit blame where uid deps rdeps graph diff ingest loop
23
+ session_open session_close
24
+ ].freeze
25
+
26
+ OPS_VERBS = %i[
27
+ boot drain pulse data_mv
28
+ key_mv_prefix key_delete_prefix
29
+ ].freeze
30
+
31
+ VERB_DOMAIN = ENTRY_VERBS.to_h { |v| [v, :entry] }
32
+ .merge(OPS_VERBS.to_h { |v| [v, :ops] }).freeze
33
+
34
+ require_relative "verb_registry/core_verbs"
35
+ CoreVerbs.each_value { |spec| register(spec) }
36
+
37
+ require_relative "../lanes/knowledge/verbs"
38
+ require_relative "../lanes/scratchpad/verbs"
39
+ require_relative "../lanes/artifact/verbs"
40
+ end
41
+ end
42
+ end
@@ -2,8 +2,8 @@ module Textus
2
2
  module Surface
3
3
  class CLI
4
4
  class Group
5
- class Rule < Group
6
- command_name "rule"
5
+ class Web < Group
6
+ command_name "web"
7
7
  end
8
8
  end
9
9
  end
@@ -13,7 +13,7 @@ module Textus
13
13
  # verb name — `spec.verb` remains the single source of dispatch.
14
14
  class Base < Verb
15
15
  class << self
16
- attr_accessor :spec
16
+ attr_accessor :spec, :stdin_json
17
17
 
18
18
  # ADR 0064: derive the CLI command name from the contract's cli_leaf
19
19
  # when not set explicitly, so an escape-hatch class never restates its
@@ -50,19 +50,18 @@ module Textus
50
50
  module_function
51
51
 
52
52
  def dispatch(verb_instance, store, spec)
53
- inputs = Textus::Dispatch::Binder.inputs_from_ordered(
53
+ inputs = Textus::Protocol::Binder.inputs_from_ordered(
54
54
  spec, verb_instance.positional, verb_instance.flag_values(spec)
55
55
  )
56
- inputs = inputs.merge(Surface::CLI::Sources.from_stdin(spec, verb_instance.stdin)) if spec.cli_stdin
56
+ inputs = inputs.merge(Surface::CLI::Sources.from_stdin(spec, verb_instance.stdin)) if verb_instance.class.stdin_json
57
57
  inputs = Surface::CLI::Sources.acquire(spec, inputs)
58
58
  inputs = apply_cli_defaults(spec, inputs)
59
59
  role = verb_instance.resolved_role(store)
60
60
 
61
61
  s = store.with_role(role)
62
- result = s.public_send(spec.verb, **inputs)
63
- result = spec.view(:cli).call(result, inputs) if spec.view(:cli)
62
+ result = Textus::Surface::Dispatch.call(s, spec.verb, **inputs)
64
63
  verb_instance.emit(result)
65
- rescue Textus::Dispatch::MissingArgs => e
64
+ rescue Textus::Protocol::MissingArgs => e
66
65
  raise UsageError.new("#{spec.cli_path} requires #{e.missing.first.wire}")
67
66
  end
68
67
 
@@ -93,7 +92,7 @@ module Textus
93
92
  if arg.type == :boolean
94
93
  effective_default(arg) == true ? "--no-#{wire}" : "--#{wire}"
95
94
  else
96
- "--#{wire}=VALUE"
95
+ "--#{wire} VALUE"
97
96
  end
98
97
  end
99
98
 
@@ -128,16 +127,7 @@ module Textus
128
127
 
129
128
  # Contract verbs whose CLI is a plain `< Verb` command, not a projection at
130
129
  # all — composite reports assembled outside the contract.
131
- # (boot removed: its contract carries surfaces :cli + the :lean arg, so the
132
- # generic projection now generates it; the hand-authored CLI::Verb::Boot is
133
- # deleted in ADR 0101.)
134
- # (doctor retained: hand-authored to preserve --check=NAME flag spelling and
135
- # the exit_code: res["ok"] ? 0 : 1 behavior — two things the generic
136
- # projection cannot yet express; kept in ADR 0101 pending a future pass.)
137
- # (fetch/fetch_all were removed in ADR 0079: Produce::Acquire::Intake is now internal,
138
- # driven by the converge sweep (drain/serve) and hook run — ADR 0089 removed the
139
- # read-through that once also drove it.)
140
- NON_PROJECTED_CLI = %i[doctor].freeze
130
+ NON_PROJECTED_CLI = %i[].freeze
141
131
 
142
132
  # The installer skips generation for either category.
143
133
  HAND_AUTHORED_VERBS = (BEHAVIORAL_HATCHES + NON_PROJECTED_CLI).freeze
@@ -146,7 +136,7 @@ module Textus
146
136
 
147
137
  def install!
148
138
  @installed ||= {}
149
- Textus::VerbRegistry.registered.each do |spec|
139
+ Textus::Protocol::VerbRegistry.registered.each do |spec|
150
140
  next unless spec.cli?
151
141
  next if hand_authored?(spec.verb)
152
142
  next if @installed[spec.verb]
@@ -156,17 +146,28 @@ module Textus
156
146
  end
157
147
  end
158
148
 
149
+ STDIN_VERBS = %i[propose].freeze
150
+ CLI_PATHS = {
151
+ uid: "key uid",
152
+ key_mv_prefix: "key mv-prefix",
153
+ key_delete_prefix: "key delete-prefix",
154
+ }.freeze
155
+
159
156
  def install_for(spec)
160
- group = spec.cli_group ? ensure_group(spec.cli_group) : nil
161
- leaf = spec.cli_leaf
157
+ cli_path = CLI_PATHS[spec.verb] || spec.cli_path
158
+ cli_words = cli_path.split
159
+ group_name = cli_words.size > 1 ? cli_words.first : nil
160
+ group = group_name ? ensure_group(group_name) : nil
161
+ leaf = cli_words.last
162
162
  non_positional = spec.args.reject(&:positional)
163
163
 
164
164
  klass = Class.new(Base)
165
165
  klass.spec = spec
166
+ klass.stdin_json = STDIN_VERBS.include?(spec.verb)
166
167
  klass.command_name leaf
167
168
  klass.parent_group group if group
168
169
  klass.option :as_flag, "--as=ROLE"
169
- klass.option :use_stdin, "--stdin" if spec.cli_stdin
170
+ klass.option :use_stdin, "--stdin" if klass.stdin_json
170
171
  non_positional.each { |a| klass.option a.name, Runner.flagspec_for(a) }
171
172
 
172
173
  # Anchor the anonymous class to a constant so descendants discovery is
@@ -25,10 +25,8 @@ module Textus
25
25
  # Parse a cli_stdin :json envelope into a by-name inputs hash, mapping
26
26
  # envelope keys (wire-names) to arg names.
27
27
  def from_stdin(spec, stream)
28
- return {} unless spec.cli_stdin == :json
29
-
30
28
  raw = stream.read.to_s
31
- return {} if raw.strip.empty? # no envelope piped -> required args surface as missing
29
+ return {} if raw.strip.empty?
32
30
 
33
31
  envelope = JSON.parse(raw)
34
32
  spec.args.each_with_object({}) do |a, h|
@@ -3,18 +3,13 @@ module Textus
3
3
  class CLI
4
4
  class Verb
5
5
  class Get < Runner::Base
6
- self.spec = Textus::VerbRegistry.for(:get)
6
+ self.spec = Textus::Protocol::VerbRegistry.for(:get)
7
7
  option :as_flag, "--as=ROLE"
8
8
 
9
9
  def invoke(store)
10
10
  key = positional.shift or raise UsageError.new("get requires a key")
11
- spec = Textus::VerbRegistry.for(:get)
12
11
  s = store.with_role(resolved_role(store))
13
- result = s.get(key: key)
14
- result = spec.view(:cli).call(result, { key: key }) if spec.view(:cli)
15
- raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
16
-
17
- emit(result)
12
+ emit(s.get(key: key))
18
13
  end
19
14
  end
20
15
  end
@@ -11,7 +11,7 @@ module Textus
11
11
 
12
12
  def call(_store)
13
13
  target = File.join(@cwd, ".textus")
14
- emit(Textus::Init.run(target, with_agent: !!with_agent))
14
+ emit(Textus::Protocol::Init.run(target, with_agent: !!with_agent))
15
15
  end
16
16
  end
17
17
  end
@@ -3,22 +3,22 @@ module Textus
3
3
  class CLI
4
4
  class Verb
5
5
  class Put < Runner::Base
6
- self.spec = Textus::VerbRegistry.for(:put)
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::VerbRegistry.for(:put)
16
- inputs = { key: key, meta: payload["_meta"] || {}, body: payload["body"] || "",
17
- content: nil, if_etag: payload["if_etag"] }
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
18
20
  s = store.with_role(resolved_role(store))
19
- result = s.put(**inputs)
20
- result = spec.view(:cli).call(result, inputs) if spec.view(:cli)
21
- emit(result)
21
+ emit(s.put(**inputs))
22
22
  end
23
23
  end
24
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
@@ -2,19 +2,19 @@ module Textus
2
2
  module Surface
3
3
  module MCP
4
4
  module Catalog
5
- PROJECTOR = Projector.new(view_key: :default).freeze
5
+ MCP_ADAPTER = Textus::Surface::MCP::Adapters::McpAdapter.new
6
6
 
7
7
  module_function
8
8
 
9
9
  def specs
10
- VerbRegistry.registered.select(&:mcp?)
10
+ Textus::Protocol::VerbRegistry.registered.select(&:mcp?)
11
11
  end
12
12
 
13
13
  def build_tools(mcp_server)
14
14
  specs.map do |spec|
15
15
  schema = spec.input_schema
16
16
  schema = schema.reject { |k, v| k == :required && Array(v).empty? }
17
- ::MCP::Tool.define(
17
+ MCP_ADAPTER.tool(
18
18
  name: spec.verb.to_s,
19
19
  description: spec.summary,
20
20
  input_schema: schema,
@@ -29,23 +29,23 @@ module Textus
29
29
  end
30
30
 
31
31
  def read_verbs
32
- VerbRegistry.registered
33
- .select { |s| s.read? && s.mcp? }
34
- .map { |s| s.verb.to_s }
32
+ Textus::Protocol::VerbRegistry.registered
33
+ .select { |s| s.read? && s.mcp? }
34
+ .map { |s| s.verb.to_s }
35
35
  end
36
36
 
37
37
  def write_verbs
38
- VerbRegistry.registered
39
- .select { |s| s.write? && s.mcp? }
40
- .map { |s| s.verb.to_s }
38
+ Textus::Protocol::VerbRegistry.registered
39
+ .select { |s| s.write? && s.mcp? }
40
+ .map { |s| s.verb.to_s }
41
41
  end
42
42
 
43
43
  def call(name, store:, args:)
44
- spec = VerbRegistry.for(name.to_sym)
44
+ spec = Textus::Protocol::VerbRegistry.for(name.to_sym)
45
45
  raise ToolError.new("unknown tool: #{name}") unless spec&.mcp?
46
46
 
47
- PROJECTOR.dispatch(name, inputs: args, store:)
48
- rescue Textus::Dispatch::MissingArgs => e
47
+ Textus::Surface::Dispatch.call(store, name.to_sym, args)
48
+ rescue Textus::Protocol::MissingArgs => e
49
49
  raise ToolError.new("#{name}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
50
50
  rescue Textus::ContractDrift, Textus::CursorExpired
51
51
  raise
@@ -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