textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -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
@@ -1,20 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Textus
2
4
  module Workflow
3
5
  class Registry
4
- def initialize
5
- @definitions = []
6
+ class UnknownWorkflow < KeyError
7
+ end
8
+
9
+ def self.load_all(root)
10
+ geometry = Textus::Protocol::Layout.new(root)
11
+ return [] unless File.directory?(geometry.workflow_dir)
12
+
13
+ Workflow::DSL::Definition.clear_registered
14
+ Dir.glob(File.join(geometry.workflow_dir, "**", "*.rb")).each { |path| load path }
15
+ Workflow::DSL::Definition.definitions
16
+ end
17
+
18
+ attr_reader :definitions
19
+
20
+ def initialize(workflow_definitions)
21
+ @definitions = workflow_definitions
22
+ @by_event = {}
23
+ @by_type = {}
24
+
25
+ workflow_definitions.each do |wf|
26
+ @by_type[wf.type] = wf
27
+ wf.handled_events.each { |ev| (@by_event[ev] ||= []) << wf }
28
+ end
6
29
  end
7
30
 
8
- def register(definition)
9
- @definitions << definition
31
+ def workflows_for(event_type)
32
+ @by_event.fetch(event_type.to_s, [])
10
33
  end
11
34
 
12
- def for(key)
13
- @definitions.find { |d| d.match?(key) }
35
+ def for(workflow_type)
36
+ @by_type[workflow_type.to_s] or raise(UnknownWorkflow.new("Unknown workflow type: #{workflow_type}"))
14
37
  end
15
38
 
16
- def all
17
- @definitions.dup
39
+ def match_for_key(key)
40
+ @definitions.find { |wf| wf.match?(key) }
18
41
  end
19
42
  end
20
43
  end
@@ -1,28 +1,38 @@
1
- require "timeout"
2
-
3
1
  module Textus
4
2
  module Workflow
3
+ class ParallelStepFailed < Textus::Error
4
+ attr_reader :failures
5
+
6
+ def initialize(failures)
7
+ @failures = failures
8
+ msgs = failures.map { |f| "#{f[:step]}: #{f[:error]}" }.join("; ")
9
+ super(:workflow_parallel_step_failed, "parallel step(s) failed: #{msgs}")
10
+ end
11
+ end
12
+
5
13
  class Runner
6
- DEFAULT_TIMEOUT = 30
14
+ Context = Data.define(:key, :entry, :config, :lane, :container, :call) do
15
+ def store_engine = container.store_engine
16
+ def manifest = container.manifest
17
+ def layout = container.layout
18
+ def file_system = container.store.file_system
19
+ def schemas = container.schemas
20
+ def read(key) = store_engine.read(key:)
21
+ def repo_root = File.dirname(container.root)
22
+ def store_root = container.root
23
+ end
7
24
 
8
25
  def initialize(definition, container:, call:)
9
26
  @definition = definition
10
27
  @container = container
11
28
  @call = call
29
+ @publisher = Publisher.new(container: container, call: call)
30
+ @step_executor = StepExecutor.new
12
31
  end
13
32
 
14
33
  def run(key)
15
- ctx = build_context(key)
16
- data = execute_steps(ctx)
17
- publish(key, data, ctx)
18
- data
19
- end
20
-
21
- private
22
-
23
- def build_context(key)
24
34
  res = @container.manifest.resolver.resolve(key)
25
- Context.new(
35
+ ctx = Context.new(
26
36
  key: key,
27
37
  entry: res.entry,
28
38
  config: {}.freeze,
@@ -30,56 +40,10 @@ module Textus
30
40
  container: @container,
31
41
  call: @call,
32
42
  )
33
- end
34
-
35
- def execute_steps(ctx)
36
- data = nil
37
- @definition.steps.each { |step| data = execute_one(step, data, ctx) }
43
+ data = @step_executor.execute(@definition.steps, ctx)
44
+ @publisher.publish(key, data, ctx, @definition) if data
38
45
  data
39
46
  end
40
-
41
- def execute_one(step, data, ctx)
42
- timeout = step.timeout || DEFAULT_TIMEOUT
43
- Timeout.timeout(timeout) { step.callable.call(data, ctx) }
44
- rescue Timeout::Error => e
45
- raise StepFailed.new(step.name, e)
46
- rescue Textus::Error
47
- raise
48
- rescue StandardError => e
49
- raise StepFailed.new(step.name, e)
50
- end
51
-
52
- def publish(key, data, ctx)
53
- blk = @definition.publish_block
54
- return blk.call(data, ctx) if blk && blk != :default
55
-
56
- built_in_publish(key, data, ctx)
57
- end
58
-
59
- def built_in_publish(key, data, ctx)
60
- normalized = Textus::Format.data_to_payload(data, ctx.entry.format)
61
- Gate::Auth.new(@container).check_action!(action: :converge, actor: @call.role, key: key)
62
- Textus::Store::Envelope::Writer.from(container: @container, call: @call).put(
63
- key,
64
- mentry: ctx.entry,
65
- payload: Textus::Store::Envelope::Writer::Payload.new(**normalized),
66
- )
67
- publish_external(key, ctx)
68
- end
69
-
70
- def publish_external(key, ctx)
71
- entry = ctx.entry
72
- return unless entry.publish_tree || !Array(entry.publish_to).empty?
73
-
74
- entry_path = @container.manifest.resolver.resolve(key).path
75
- return unless entry.publish_tree || File.exist?(entry_path)
76
-
77
- reader = Textus::Store::Envelope::Reader.from(container: @container)
78
- pctx = Textus::Manifest::Entry::Base::PublishContext.new(
79
- container: @container, call: @call, reader: reader.method(:read),
80
- )
81
- entry.publish_via(pctx)
82
- end
83
47
  end
84
48
  end
85
49
  end
@@ -0,0 +1,45 @@
1
+ module Textus
2
+ module Workflow
3
+ class Scheduler
4
+ def initialize(registry:, workflow_queue:, reader:)
5
+ @registry = registry
6
+ @workflow_queue = workflow_queue
7
+ @reader = reader
8
+ end
9
+
10
+ def seed_expired
11
+ seeded = 0
12
+ @registry.definitions.each do |wf|
13
+ next unless wf.ttl && wf.save_to
14
+
15
+ lane = wf.save_to.split(".").first
16
+ next if @workflow_queue.pending?(event_type: "workflow", key: wf.save_to)
17
+
18
+ entry = @reader.call(key: wf.save_to)
19
+ unless entry
20
+ @workflow_queue.push(
21
+ event_type: "workflow", workflow_type: wf.type,
22
+ key: wf.save_to, lane: lane
23
+ )
24
+ seeded += 1
25
+ next
26
+ end
27
+
28
+ meta = entry.respond_to?(:meta) ? entry.meta : (entry["_meta"] || {})
29
+ generated_at = meta["generated_at"]
30
+ next unless generated_at
31
+
32
+ age = Time.now - Time.parse(generated_at)
33
+ next unless age > wf.ttl
34
+
35
+ @workflow_queue.push(
36
+ event_type: "workflow", workflow_type: wf.type,
37
+ key: wf.save_to, lane: lane
38
+ )
39
+ seeded += 1
40
+ end
41
+ seeded
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,78 @@
1
+ require "timeout"
2
+ require "concurrent"
3
+
4
+ module Textus
5
+ module Workflow
6
+ class StepExecutor
7
+ DEFAULT_TIMEOUT = 30
8
+ CONCURRENCY_ADAPTER = Textus::Infra::Adapters::ConcurrencyAdapter.new
9
+
10
+ class StepScope
11
+ include Textus::Workflow::StepHelpers
12
+
13
+ def initialize(data, ctx)
14
+ @data = data
15
+ @ctx = ctx
16
+ end
17
+
18
+ def call(callable)
19
+ instance_exec(@data, @ctx, &callable)
20
+ end
21
+ end
22
+
23
+ def execute(steps, ctx)
24
+ data = nil
25
+ steps.each { |step| data = execute_one(step, data, ctx) }
26
+ data
27
+ end
28
+
29
+ private
30
+
31
+ def execute_one(step, data, ctx)
32
+ case step
33
+ when DSL::Step then execute_single(step, data, ctx)
34
+ when DSL::Parallel then execute_parallel(step, ctx)
35
+ else raise ArgumentError.new("unknown step type: #{step.class}")
36
+ end
37
+ end
38
+
39
+ def execute_single(step, data, ctx)
40
+ scope = StepScope.new(data, ctx)
41
+ timeout = step.timeout || DEFAULT_TIMEOUT
42
+ Timeout.timeout(timeout) { scope.call(step.callable) }
43
+ rescue Timeout::Error => e
44
+ raise StepFailed.new(step.name, e)
45
+ rescue Textus::Error
46
+ raise
47
+ rescue StandardError => e
48
+ raise StepFailed.new(step.name, e)
49
+ end
50
+
51
+ def execute_parallel(parallel, ctx)
52
+ scope = StepScope.new(nil, ctx)
53
+ promises = parallel.steps.map do |step|
54
+ CONCURRENCY_ADAPTER.future do
55
+ timeout = step.timeout || DEFAULT_TIMEOUT
56
+ Timeout.timeout(timeout) { scope.call(step.callable) }
57
+ end
58
+ end
59
+
60
+ results = CONCURRENCY_ADAPTER.zip_futures(*promises).value!
61
+ failures = []
62
+ outputs = {}
63
+ results.each_with_index do |result, i|
64
+ step = parallel.steps[i]
65
+ if result.fulfilled?
66
+ outputs[step.name] = result.value!
67
+ else
68
+ failures << { step: step.name, error: result.reason.message }
69
+ end
70
+ end
71
+
72
+ raise ParallelStepFailed.new(failures) unless failures.empty?
73
+
74
+ outputs
75
+ end
76
+ end
77
+ end
78
+ end