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,117 @@
1
+ require "yaml"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Schema
6
+ attr_reader :name, :required, :optional, :fields, :raw
7
+
8
+ def self.load(path)
9
+ raw = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time], aliases: false)
10
+ new(raw)
11
+ end
12
+
13
+ def initialize(raw)
14
+ @raw = raw || {}
15
+ @name = @raw["name"]
16
+ @required = Array(@raw["required"])
17
+ @optional = Array(@raw["optional"])
18
+ @fields = @raw["fields"] || {}
19
+ end
20
+
21
+ def to_h
22
+ @raw
23
+ end
24
+
25
+ def maintained_by(field)
26
+ meta = @fields[field] or return nil
27
+ meta["maintained_by"]
28
+ end
29
+
30
+ def unowned_fields
31
+ @fields.each_with_object([]) do |(name, spec), acc|
32
+ next unless spec.is_a?(Hash)
33
+
34
+ acc << name if spec["maintained_by"].nil?
35
+ end
36
+ end
37
+
38
+ def evolution
39
+ raw = @raw["evolution"] || {}
40
+ raw.each_with_object({}) do |(k, v), h|
41
+ h[k] = v.is_a?(Date) || v.is_a?(Time) ? v.to_s : v
42
+ end
43
+ end
44
+
45
+ def validate!(frontmatter)
46
+ missing = @required - frontmatter.keys
47
+ raise Textus::SchemaViolation.new("missing" => missing) unless missing.empty?
48
+
49
+ known = (@required + @optional).uniq
50
+ frontmatter.each do |k, v|
51
+ next unless @fields.key?(k)
52
+
53
+ check_type!(k, v, @fields[k])
54
+ end
55
+
56
+ warnings = frontmatter.keys - known
57
+ warnings.map { |w| "unknown field: #{w}" }
58
+ end
59
+
60
+ TYPE_VALIDATORS = {
61
+ "string" => :validate_string,
62
+ "number" => :validate_number,
63
+ "boolean" => :validate_boolean,
64
+ "enum" => :validate_enum,
65
+ "array" => :validate_array,
66
+ "object" => :validate_object,
67
+ }.freeze
68
+
69
+ private
70
+
71
+ def check_type!(field, value, spec)
72
+ type = spec["type"]
73
+ return unless type
74
+
75
+ send(TYPE_VALIDATORS[type], field, value, spec)
76
+ end
77
+
78
+ def validate_string(field, value, spec)
79
+ bad!(field, "expected string") unless value.is_a?(String)
80
+ return unless (max = spec["max"]) && value.bytesize > max
81
+
82
+ bad!(field, "exceeds max #{max}")
83
+ end
84
+
85
+ def validate_number(field, value, _spec)
86
+ bad!(field, "expected number") unless value.is_a?(Numeric)
87
+ end
88
+
89
+ def validate_boolean(field, value, _spec)
90
+ bad!(field, "expected boolean") unless [true, false].include?(value)
91
+ end
92
+
93
+ def validate_enum(field, value, spec)
94
+ values = Array(spec["values"])
95
+ bad!(field, "not in enum #{values.inspect}") unless values.include?(value)
96
+ end
97
+
98
+ def validate_array(field, value, spec)
99
+ bad!(field, "expected array") unless value.is_a?(Array)
100
+ return unless (items = spec["items"])
101
+
102
+ value.each_with_index { |v, i| check_type!("#{field}[#{i}]", v, items) }
103
+ end
104
+
105
+ def validate_object(field, value, spec)
106
+ bad!(field, "expected object") unless value.is_a?(Hash)
107
+ return unless (sub = spec["fields"])
108
+
109
+ sub.each { |fk, fspec| check_type!("#{field}.#{fk}", value[fk], fspec) if value.key?(fk) }
110
+ end
111
+
112
+ def bad!(field, reason)
113
+ raise Textus::SchemaViolation.new("field" => field, "reason" => reason)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,11 @@
1
+ Textus::Protocol::Session::Bridge = Data.define(:container, :gate) do
2
+ def store = container.store
3
+ def manifest = container.manifest
4
+ def schemas = container.schemas
5
+ def layout = container.layout
6
+ def workflows = container.workflows
7
+ def root = container.root
8
+ def ttl_evaluator = container.ttl_evaluator
9
+
10
+ def dispatch(...) = gate.dispatch(...)
11
+ end
@@ -0,0 +1,5 @@
1
+ Textus::Protocol::Session::Context = Data.define(:role, :correlation_id, :dry_run, :cursor, :contract_etag, :propose_lane) do
2
+ def call
3
+ Textus::Value::Call.build(role:, correlation_id:)
4
+ end
5
+ end
@@ -0,0 +1,87 @@
1
+ require "forwardable"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Session
6
+ extend Forwardable
7
+
8
+ attr_reader :context, :bridge
9
+
10
+ DOMAIN_VERBS = (VerbRegistry::ENTRY_VERBS + VerbRegistry::OPS_VERBS).to_set.freeze
11
+
12
+ def_delegators :context, :role, :cursor, :correlation_id, :contract_etag, :propose_lane
13
+ def_delegator :context, :dry_run, :dry_run?
14
+ def_delegators :bridge, :root, :container, :store, :manifest, :schemas, :layout, :workflows, :gate, :ttl_evaluator
15
+
16
+ def self.open(root, role: Value::Role::DEFAULT, correlation_id: nil, dry_run: false)
17
+ root = File.expand_path(root)
18
+ container = Protocol::Builder.new.call(root)
19
+ gate = container.gate
20
+ bridge = Session::Bridge.new(container:, gate:)
21
+ context = Session::Context.new(
22
+ role: role.to_s,
23
+ correlation_id: correlation_id || SecureRandom.uuid,
24
+ dry_run: dry_run,
25
+ cursor: container.store.audit_log.latest[:seq],
26
+ contract_etag: Value::Etag.for_contract(bridge.layout),
27
+ propose_lane: container.manifest.policy.propose_lane_for(role.to_s),
28
+ )
29
+ new(bridge:, context:)
30
+ end
31
+
32
+ def initialize(bridge:, context:)
33
+ @bridge = bridge
34
+ @context = context
35
+ end
36
+
37
+ def method_missing(name, *args, **kwargs)
38
+ if DOMAIN_VERBS.include?(name)
39
+ raise ArgumentError.new("#{name} accepts keyword arguments only") unless args.empty?
40
+
41
+ return bridge.dispatch(role.to_sym, name, kwargs, call: context.call)
42
+ end
43
+
44
+ return bridge.public_send(name, *args, **kwargs) if bridge.respond_to?(name)
45
+
46
+ super
47
+ end
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ DOMAIN_VERBS.include?(name) || bridge.respond_to?(name) || super
51
+ end
52
+
53
+ def with_correlation_id(cid)
54
+ self.class.new(bridge:, context: context.with(correlation_id: cid))
55
+ end
56
+
57
+ def with_role(new_role)
58
+ self.class.new(
59
+ bridge:,
60
+ context: context.with(
61
+ role: new_role.to_s,
62
+ cursor: container.store.audit_log.latest[:seq],
63
+ contract_etag: Value::Etag.for_contract(container.layout),
64
+ propose_lane: manifest.policy.propose_lane_for(new_role.to_s),
65
+ ),
66
+ )
67
+ end
68
+
69
+ def advance_cursor(new_cursor)
70
+ @context = context.with(cursor: new_cursor)
71
+ self
72
+ end
73
+
74
+ def check_etag!(observed_etag)
75
+ return if observed_etag == contract_etag
76
+
77
+ raise Textus::ContractDrift.new(
78
+ "contract changed (was #{short_etag(contract_etag)}, now #{short_etag(observed_etag)})",
79
+ )
80
+ end
81
+
82
+ private
83
+
84
+ def short_etag(etag) = etag.to_s.delete_prefix("sha256:")[0, 8]
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ module Textus
2
+ module Protocol
3
+ class StoreEngine
4
+ module Delete
5
+ module_function
6
+
7
+ def call(key, deps:, if_etag: nil)
8
+ path = Pipeline.resolve_path(key, deps)
9
+ assert_exists!(key, path, deps)
10
+ etag_before = Pipeline.check_etag(key, path, if_etag, deps.file_system)
11
+ deps.file_system.delete(path)
12
+ deps.entry_index.remove(key:)
13
+ Pipeline.prune_parents(path, deps)
14
+ append_audit(key, etag_before, deps)
15
+ Pipeline.emit_event(deps, event_type: "entry.deleted",
16
+ pending_key: key, emit_key: key)
17
+ nil
18
+ end
19
+
20
+ def assert_exists!(key, path, deps)
21
+ return if deps.file_system.exists?(path)
22
+
23
+ raise Textus::UnknownKey.new(key,
24
+ suggestions: deps.manifest.resolver.suggestions_for(key))
25
+ end
26
+
27
+ def append_audit(key, etag_before, deps)
28
+ deps.audit_log.append(
29
+ role: deps.call.role, verb: "key_delete", key:,
30
+ etag_before:, etag_after: nil,
31
+ correlation_id: deps.call.correlation_id
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,109 @@
1
+ module Textus
2
+ module Protocol
3
+ class StoreEngine
4
+ module Move
5
+ MoveResult = Data.define(:from_key, :to_key, :from_path, :to_path, :dry_run, :envelope)
6
+
7
+ module_function
8
+
9
+ def call(from_key:, to_key:, deps:, if_etag: nil, dry_run: false)
10
+ validate!(from_key, to_key)
11
+ from_path, to_path, from_entry, to_entry = resolve_paths(from_key, to_key, deps)
12
+ assert_source_exists!(from_key, from_path, deps)
13
+ assert_target_not_exists!(to_path)
14
+ check_cross_zone!(from_entry, to_entry)
15
+ ensure_uid!(from_key, from_entry, from_path, deps)
16
+ etag_before = Pipeline.check_etag(from_key, from_path, if_etag, deps.file_system)
17
+
18
+ if dry_run
19
+ return MoveResult.new(from_key:, to_key:, from_path:, to_path:,
20
+ dry_run: true, envelope: nil)
21
+ end
22
+
23
+ deps.file_system.mv(from_path, to_path)
24
+ Pipeline.prune_parents(from_path, deps)
25
+ rewrite_basename!(to_key, to_path, to_entry)
26
+ etag_after = Value::Etag.for_file(to_path)
27
+ envelope = deps.reader.read(key: to_key)
28
+ append_audit(to_key, etag_before, etag_after, deps)
29
+ reindex_entry(from_key, to_key, to_entry, envelope, deps)
30
+ Pipeline.emit_event(deps, event_type: "entry.moved",
31
+ pending_key: from_key, emit_key: to_key,
32
+ emit_lane: to_entry.lane,
33
+ pending_keys: [from_key, to_key])
34
+ MoveResult.new(from_key:, to_key:, from_path:, to_path:,
35
+ dry_run: false, envelope:)
36
+ end
37
+
38
+ def validate!(from_key, to_key)
39
+ Manifest::Data.validate_key!(from_key)
40
+ Manifest::Data.validate_key!(to_key)
41
+ raise UsageError.new("old and new keys are identical") if from_key == to_key
42
+ end
43
+
44
+ def resolve_paths(from_key, to_key, deps)
45
+ from = deps.manifest.resolver.resolve(from_key)
46
+ to = deps.manifest.resolver.resolve(to_key)
47
+ [from.path, to.path, from.entry, to.entry]
48
+ end
49
+
50
+ def assert_source_exists!(key, path, deps)
51
+ return if deps.file_system.exists?(path)
52
+
53
+ raise UnknownKey.new(key, suggestions: deps.manifest.resolver.suggestions_for(key))
54
+ end
55
+
56
+ def assert_target_not_exists!(path)
57
+ return unless path && File.exist?(path)
58
+
59
+ raise UsageError.new("target already exists")
60
+ end
61
+
62
+ def check_cross_zone!(from_entry, to_entry)
63
+ return if from_entry.lane == to_entry.lane
64
+
65
+ raise UsageError.new("cross-zone moves forbidden (#{from_entry.lane} → #{to_entry.lane})")
66
+ end
67
+
68
+ def ensure_uid!(_key, entry, path, deps)
69
+ return if Infra::Store::Envelope::Meta::NO_META_FORMATS.include?(entry.format)
70
+ return unless path && deps.file_system.exists?(path)
71
+
72
+ raw = deps.file_system.read(path)
73
+ parsed = Format.for(entry.format).parse(raw, path:)
74
+ meta = parsed["_meta"] || {}
75
+ return if meta["uid"].is_a?(String) && !meta["uid"].empty?
76
+
77
+ new_meta, = Infra::Store::Envelope::Meta.inject_all(meta, parsed["content"], {},
78
+ format: entry.format)
79
+ bytes, = Format.for(entry.format).serialize_for_put(
80
+ meta: new_meta, body: parsed["body"] || "", content: parsed["content"], path:,
81
+ )
82
+ deps.file_system.write(path, bytes)
83
+ end
84
+
85
+ def rewrite_basename!(key, path, entry)
86
+ basename = key.split(".").last
87
+ Format.for(entry.format).rewrite_name(path, basename)
88
+ end
89
+
90
+ def append_audit(key, etag_before, etag_after, deps)
91
+ deps.audit_log.append(
92
+ role: deps.call.role, verb: "key_mv", key:,
93
+ etag_before:, etag_after:,
94
+ correlation_id: deps.call.correlation_id
95
+ )
96
+ end
97
+
98
+ def reindex_entry(from_key, to_key, to_entry, envelope, deps)
99
+ deps.entry_index.remove(key: from_key)
100
+ content = envelope.content ? envelope.content.to_json : envelope.body
101
+ deps.entry_index.record(
102
+ key: to_key, lane: to_entry.lane, format: to_entry.format,
103
+ schema: to_entry.schema, content:
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,61 @@
1
+ module Textus
2
+ module Protocol
3
+ class StoreEngine
4
+ module Pipeline
5
+ WriteDeps = Data.define(:file_system, :manifest, :store, :call, :reader, :layout, :event_emitter, :schemas) do
6
+ def audit_log = store.audit_log
7
+ def entry_index = store.entry_index
8
+ def file_store = store.file_store
9
+ end
10
+
11
+ DeleteDeps = Data.define(:file_system, :manifest, :store, :call, :layout, :event_emitter) do
12
+ def audit_log = store.audit_log
13
+ def entry_index = store.entry_index
14
+ def workflow_queue = store.workflow_queue
15
+ end
16
+
17
+ MoveDeps = Data.define(:file_system, :manifest, :store, :call, :reader, :layout, :event_emitter) do
18
+ def audit_log = store.audit_log
19
+ def entry_index = store.entry_index
20
+ def workflow_queue = store.workflow_queue
21
+ end
22
+
23
+ module_function
24
+
25
+ def resolve_path(key, deps)
26
+ deps.manifest.resolver.resolve(key).path
27
+ end
28
+
29
+ def check_etag(key, path, if_etag, file_system)
30
+ etag_before = file_system.exists?(path) ? file_system.etag(path) : nil
31
+ raise Textus::EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
32
+
33
+ etag_before
34
+ end
35
+
36
+ def prune_parents(path, deps)
37
+ floor = deps.layout.lane_floor(path)
38
+ return unless floor
39
+
40
+ dir = File.dirname(path)
41
+ while dir.start_with?("#{floor}/") && deps.file_system.dir_empty?(dir)
42
+ deps.file_system.rmdir(dir)
43
+ dir = File.dirname(dir)
44
+ end
45
+ rescue SystemCallError
46
+ # race: dir may have been emptied by another writer
47
+ end
48
+
49
+ def emit_event(deps, event_type:, pending_key:, emit_key:, emit_lane: nil, pending_keys: [])
50
+ return unless deps.event_emitter
51
+ return if deps.store.workflow_queue.pending?(event_type:, key: pending_key)
52
+ return if pending_keys.any? { |k| deps.store.workflow_queue.pending?(event_type:, key: k) }
53
+
54
+ deps.event_emitter.emit(event_type:, key: emit_key, lane: emit_lane || "artifacts")
55
+ rescue StandardError => e
56
+ warn "textus: emit_workflow_event(#{event_type}, #{emit_key}) failed: #{e.message}"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,104 @@
1
+ module Textus
2
+ module Protocol
3
+ class StoreEngine
4
+ module Put
5
+ module_function
6
+
7
+ def call(key, mentry:, path:, payload:, deps:, if_etag: nil, fmt: nil)
8
+ format = fmt || mentry.format
9
+
10
+ validate_input(key, payload, format, mentry, path)
11
+ validate_schema(payload, mentry, deps)
12
+
13
+ existing_env = read_existing(key, mentry, path, format, deps)
14
+ meta, content = inject_meta(payload, existing_env, format, deps)
15
+ bytes, eff_meta, eff_body, eff_content = serialize(payload, meta, content, path, format)
16
+
17
+ etag_before = Pipeline.check_etag(key, path, if_etag, deps.file_system)
18
+ deps.file_system.write(path, bytes)
19
+ envelope = Envelope.build(
20
+ key:, mentry:, path:,
21
+ meta: eff_meta, body: eff_body,
22
+ etag: Value::Etag.for_bytes(bytes),
23
+ content: eff_content
24
+ )
25
+
26
+ append_audit(key, etag_before, envelope, deps)
27
+ index_entry(key, mentry, format, eff_body, eff_content, deps)
28
+ Pipeline.emit_event(deps, event_type: "entry.written",
29
+ pending_key: key, emit_key: key,
30
+ emit_lane: mentry.lane)
31
+ envelope
32
+ end
33
+
34
+ def validate_input(key, payload, format, mentry, path)
35
+ EntryConstraint.validate_naming!(key, mentry)
36
+ fmt = Format.for(format)
37
+ fmt.enforce_name_match!(path, payload.meta)
38
+ fmt.validate_raw_entry!(
39
+ { "_meta" => payload.meta, "content" => payload.content },
40
+ mentry.lane,
41
+ )
42
+ end
43
+
44
+ def validate_schema(payload, mentry, deps)
45
+ EntryConstraint.validate_schema!(payload, mentry, schemas: deps.schemas)
46
+ end
47
+
48
+ def read_existing(key, mentry, path, format, deps)
49
+ return nil unless path && deps.file_system.exists?(path)
50
+
51
+ raw = deps.file_system.read(path)
52
+ parsed = Format.for(format).parse(raw, path:)
53
+ Envelope.build(
54
+ key:, mentry:, path:,
55
+ meta: parsed["_meta"] || {},
56
+ body: parsed["body"] || "",
57
+ content: parsed["content"],
58
+ etag: Value::Etag.for_file(path)
59
+ )
60
+ end
61
+
62
+ def inject_meta(payload, existing_env, format, deps)
63
+ existing_meta = existing_env ? existing_env.meta : {}
64
+ Infra::Store::Envelope::Meta.inject_all(
65
+ payload.meta || {}, payload.content, existing_meta,
66
+ format:,
67
+ etag_for: method(:etag_for_key).curry.call(deps)
68
+ )
69
+ end
70
+
71
+ def etag_for_key(deps, key)
72
+ path = deps.manifest.resolver.resolve(key).path
73
+ return nil unless deps.file_system.exists?(path)
74
+
75
+ Value::Etag.for_file(path)
76
+ rescue Textus::Error
77
+ nil
78
+ end
79
+
80
+ def serialize(payload, meta, content, path, format)
81
+ Format.for(format).serialize_for_put(
82
+ meta:, body: payload.body, content:, path:,
83
+ )
84
+ end
85
+
86
+ def append_audit(key, etag_before, envelope, deps)
87
+ deps.audit_log.append(
88
+ role: deps.call.role, verb: "put", key:,
89
+ etag_before:, etag_after: envelope.etag,
90
+ correlation_id: deps.call.correlation_id
91
+ )
92
+ end
93
+
94
+ def index_entry(key, mentry, format, eff_body, eff_content, deps)
95
+ content = eff_content ? eff_content.to_json : eff_body
96
+ deps.entry_index.record(
97
+ key:, lane: mentry.lane, format:,
98
+ schema: mentry.schema, content:
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,86 @@
1
+ module Textus
2
+ module Protocol
3
+ class StoreEngine
4
+ def initialize(file_system:, manifest:, layout:, store:, event_emitter: nil, schemas: nil)
5
+ @file_system = file_system
6
+ @manifest = manifest
7
+ @layout = layout
8
+ @store = store
9
+ @event_emitter = event_emitter
10
+ @schemas = schemas
11
+ end
12
+
13
+ def read(key:)
14
+ result = resolve(key:)
15
+ path = result.path
16
+ return nil unless path && @file_system.exists?(path)
17
+
18
+ raw = @file_system.read(path)
19
+ parsed = Format.for(result.entry.format).parse(raw, path:)
20
+ Envelope.build(key:, mentry: result.entry, path:,
21
+ meta: parsed["_meta"] || {},
22
+ body: parsed["body"] || "",
23
+ content: parsed["content"],
24
+ etag: Value::Etag.for_file(path))
25
+ end
26
+
27
+ def exists?(key:)
28
+ path = resolve(key:).path
29
+ path && @file_system.exists?(path)
30
+ end
31
+
32
+ def list(prefix: nil, lane: nil, q: nil, schema: nil)
33
+ if @store.entry_index && (q || schema)
34
+ rows = @store.entry_index.search(query: q, schema:, lane:, prefix:)
35
+ (rows || []).map { |r| { "key" => r["key"], "lane" => r["lane"] } }
36
+ else
37
+ rows = @manifest.resolver.enumerate(prefix:)
38
+ rows = rows.select { |r| r[:manifest_entry].lane == lane } if lane
39
+ rows.map { |r| { "key" => r[:key], "lane" => r[:manifest_entry].lane } }
40
+ end
41
+ end
42
+
43
+ def resolve(key:) = @manifest.resolver.resolve(key)
44
+ def mentry_for(key:)= resolve(key:).entry
45
+
46
+ def put(key:, meta:, body:, content:, call:, if_etag: nil, format: nil)
47
+ result = resolve(key:)
48
+ payload = Value::Payload.new(meta: meta || {}, body:, content:)
49
+ deps = build_deps(reader: nil, call:)
50
+
51
+ if format
52
+ fmt_mod = Textus::Protocol::Format.for(format)
53
+ path = result.path.sub(/\.[^.]+$/, "") + fmt_mod.nested_ext(result.remaining.last)
54
+ Put.call(key, mentry: result.entry, path:, payload:, if_etag:, deps:, fmt: format)
55
+ else
56
+ Put.call(key, mentry: result.entry, path: result.path, payload:, if_etag:, deps:)
57
+ end
58
+ end
59
+
60
+ def delete(key:, call:, if_etag: nil)
61
+ deps = Pipeline::DeleteDeps.new(
62
+ file_system: @file_system, manifest: @manifest, store: @store,
63
+ layout: @layout, event_emitter: @event_emitter, call:
64
+ )
65
+ Delete.call(key, if_etag:, deps:)
66
+ end
67
+
68
+ def move(from_key:, to_key:, call:, if_etag: nil, dry_run: false)
69
+ deps = Pipeline::MoveDeps.new(
70
+ file_system: @file_system, manifest: @manifest, store: @store,
71
+ layout: @layout, event_emitter: @event_emitter, reader: self, call:
72
+ )
73
+ Move.call(from_key:, to_key:, if_etag:, dry_run:, deps:)
74
+ end
75
+
76
+ private
77
+
78
+ def build_deps(reader:, call:)
79
+ Pipeline::WriteDeps.new(
80
+ file_system: @file_system, manifest: @manifest, store: @store,
81
+ layout: @layout, event_emitter: @event_emitter, schemas: @schemas, reader:, call:
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ module Textus
2
+ module Protocol
3
+ module V4
4
+ LANES = {
5
+ "knowledge" => { writers: %w[human],
6
+ desc: "the maintained source of truth about the repo",
7
+ retention_ttl: nil },
8
+ "scratchpad" => { writers: %w[human agent],
9
+ desc: "the agent's own durable scratchpad (working memory)",
10
+ retention_ttl: nil },
11
+ "artifacts" => { writers: %w[automation],
12
+ desc: "machine-maintained computed outputs",
13
+ retention_ttl: 3600 * 24 * 30 },
14
+ "raw" => { writers: %w[human agent automation],
15
+ desc: "ingested external source material (write-once)",
16
+ retention_ttl: nil },
17
+ }.freeze
18
+
19
+ NAMING_PATTERNS = {
20
+ "sequential" => /\A\d{4}-[a-z0-9-]+\.\w+\z/,
21
+ "dated" => /\A\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.\w+\z/,
22
+ }.freeze
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ module VerbRegistry
4
+ ArgSpec = Data.define(
5
+ :name, :type, :required, :positional,
6
+ :description, :wire_name, :default, :source, :coerce, :cli_default
7
+ ) do
8
+ def wire = wire_name || name
9
+
10
+ def self.arg(name:, type: String, required: false, positional: false,
11
+ description: nil, wire_name: nil,
12
+ default: nil, source: nil, coerce: nil, cli_default: nil)
13
+ new(name:, type:, required:, positional:,
14
+ description:, wire_name:, default:, source:, coerce:, cli_default:)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end