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,17 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ class Infrastructure
5
+ Components = Data.define(:database, :file_system, :store, :layout)
6
+
7
+ def call(root)
8
+ layout = Layout.new(root)
9
+ database = Infra::Database.new(layout:).setup!
10
+ file_system = Infra::FileSystem.new
11
+ store = Infra::Store.new(database:, file_system:)
12
+ Components.new(database:, file_system:, store:, layout:)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ class Runtime
5
+ Components = Data.define(:store_engine, :gate, :ttl_evaluator, :container)
6
+
7
+ def call(infra:, domain:, root:)
8
+ event_emitter = Textus::Workflow::EventEmitter.new(
9
+ infra.store.workflow_queue,
10
+ registry: domain.workflow_registry,
11
+ )
12
+
13
+ store_engine = StoreEngine.new(
14
+ file_system: infra.file_system, manifest: domain.manifest,
15
+ layout: infra.layout, store: infra.store,
16
+ event_emitter:, schemas: domain.schemas
17
+ )
18
+
19
+ ttl_evaluator = Freshness::TtlEvaluator.new(
20
+ manifest: domain.manifest,
21
+ file_stat: infra.file_system,
22
+ clock: Infra::Clock.new,
23
+ )
24
+
25
+ container = Container.new(
26
+ store: infra.store, layout: infra.layout,
27
+ manifest: domain.manifest, schemas: domain.schemas,
28
+ workflows: domain.workflows,
29
+ store_engine: store_engine,
30
+ root:, ttl_evaluator:
31
+ )
32
+
33
+ gate = Gate.new(
34
+ manifest: domain.manifest,
35
+ resolver: domain.manifest.resolver,
36
+ container:,
37
+ store_engine:,
38
+ )
39
+
40
+ container.gate = gate
41
+ Components.new(store_engine:, gate:, ttl_evaluator:, container:)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ module Textus
2
+ module Protocol
3
+ class Builder
4
+ def call(root)
5
+ infra = Infrastructure.new.call(root)
6
+ domain = Domain.new.call(root, layout: infra.layout)
7
+ Runtime.new.call(infra:, domain:, root:).container
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module Textus
2
+ module Protocol
3
+ class Container
4
+ attr_accessor :gate
5
+ attr_reader :store, :layout, :manifest, :schemas, :workflows, :workflow_registry, :root, :ttl_evaluator, :store_engine
6
+
7
+ def initialize(store:, layout:, manifest:, schemas:, workflows:,
8
+ root: nil, gate: nil, ttl_evaluator: nil, store_engine: nil)
9
+ @store = store
10
+ @layout = layout
11
+ @manifest = manifest
12
+ @schemas = schemas
13
+ @workflows = workflows
14
+ @workflow_registry = workflows ? Textus::Workflow::Registry.new(workflows) : nil
15
+ @root = root
16
+ @gate = gate
17
+ @ttl_evaluator = ttl_evaluator
18
+ @store_engine = store_engine
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,170 @@
1
+ module Textus
2
+ module Protocol
3
+ module Diff
4
+ module_function
5
+
6
+ def body(a_text, b_text)
7
+ a_lines = (a_text || "").lines.map(&:chomp)
8
+ b_lines = (b_text || "").lines.map(&:chomp)
9
+ hunks = build_hunks(myers_diff(a_lines, b_lines), a_lines, b_lines)
10
+ { "type" => "body", "hunks" => hunks, "stats" => stats(hunks) }
11
+ end
12
+
13
+ def meta(a_hash, b_hash)
14
+ a = a_hash || {}
15
+ b = b_hash || {}
16
+ all_keys = (a.keys + b.keys).uniq.sort
17
+ changes = all_keys.filter_map do |k|
18
+ av = a[k]
19
+ bv = b[k]
20
+ next if av == bv
21
+
22
+ { "key" => k.to_s, "old" => av, "new" => bv }
23
+ end
24
+ { "type" => "meta", "changes" => changes, "stats" => { "changed" => changes.size } }
25
+ end
26
+
27
+ def schema(a_schema, b_schema)
28
+ meta(a_schema, b_schema).merge("type" => "schema")
29
+ end
30
+
31
+ def summary(diff_result)
32
+ parts = []
33
+ parts << "#{diff_result.dig("body", "stats", "additions") || 0}+" if diff_result["body"]
34
+ parts << "#{diff_result.dig("body", "stats", "deletions") || 0}-" if diff_result["body"]
35
+ parts << "#{diff_result.dig("meta", "stats", "changed") || 0}~" if diff_result["meta"]
36
+ parts.join(" ")
37
+ end
38
+
39
+ def myers_diff(a_lines, b_lines)
40
+ a_count = a_lines.size
41
+ b_count = b_lines.size
42
+ max_d = a_count + b_count
43
+ v = Hash.new(0)
44
+ v[1] = 0
45
+ trace = []
46
+
47
+ (0..max_d).each do |d|
48
+ (-d..d).step(2).each do |k|
49
+ x = if k == -d || (k != d && v[k - 1] < v[k + 1])
50
+ v[k + 1]
51
+ else
52
+ v[k - 1] + 1
53
+ end
54
+ y = x - k
55
+ while x < a_count && y < b_count && a_lines[x] == b_lines[y]
56
+ x += 1
57
+ y += 1
58
+ end
59
+ v[k] = x
60
+ return backtrack(trace, x, y) if x >= a_count && y >= b_count
61
+ end
62
+ trace << v.dup
63
+ end
64
+ []
65
+ end
66
+
67
+ def backtrack(trace, x_pos, y_pos)
68
+ script = []
69
+ d = trace.size - 1
70
+ return script if d.negative?
71
+
72
+ k = x_pos - y_pos
73
+ while d.positive?
74
+ prev_v = trace[d - 1]
75
+ prev_k = if k == -d || (k != d && (prev_v[k - 1] || -1) < (prev_v[k + 1] || -1))
76
+ k + 1
77
+ else
78
+ k - 1
79
+ end
80
+ prev_x = prev_v[prev_k] || 0
81
+ prev_y = prev_x - prev_k
82
+
83
+ while x_pos > prev_x && y_pos > prev_y
84
+ script.unshift([:eq, x_pos - 1, y_pos - 1])
85
+ x_pos -= 1
86
+ y_pos -= 1
87
+ end
88
+
89
+ if x_pos > prev_x
90
+ script.unshift([:del, x_pos - 1, nil])
91
+ x_pos -= 1
92
+ elsif y_pos > prev_y
93
+ script.unshift([:ins, nil, y_pos - 1])
94
+ y_pos -= 1
95
+ end
96
+
97
+ d -= 1
98
+ k = prev_k
99
+ x_pos = prev_x
100
+ y_pos = prev_y
101
+ end
102
+
103
+ script
104
+ end
105
+
106
+ def build_hunks(script, a_lines, b_lines)
107
+ return [] if script.empty?
108
+
109
+ hunks = []
110
+ i = 0
111
+ while i < script.size
112
+ next_context = find_next_change(script, i)
113
+ break unless next_context
114
+
115
+ start = [next_context - 3, 0].max
116
+ hunk_end_script = find_hunk_end(script, next_context)
117
+ hunk_end = [hunk_end_script + 3, script.size - 1].min
118
+
119
+ a_start = nil
120
+ b_start = nil
121
+ a_lines_hunk = []
122
+ b_lines_hunk = []
123
+
124
+ (start..hunk_end).each do |j|
125
+ op, ai, bi = script[j]
126
+ a_start ||= ai if ai
127
+ b_start ||= bi if bi
128
+
129
+ case op
130
+ when :eq
131
+ a_lines_hunk << { "type" => "context", "line" => a_lines[ai] }
132
+ b_lines_hunk << { "type" => "context", "line" => b_lines[bi] }
133
+ when :del
134
+ a_lines_hunk << { "type" => "deletion", "line" => a_lines[ai] }
135
+ when :ins
136
+ b_lines_hunk << { "type" => "addition", "line" => b_lines[bi] }
137
+ end
138
+ end
139
+
140
+ hunks << {
141
+ "a_start" => a_start, "b_start" => b_start,
142
+ "a_lines" => a_lines_hunk, "b_lines" => b_lines_hunk
143
+ }
144
+
145
+ i = hunk_end + 1
146
+ end
147
+
148
+ hunks
149
+ end
150
+
151
+ def find_next_change(script, from)
152
+ (from...script.size).find { |i| script[i][0] != :eq }
153
+ end
154
+
155
+ def find_hunk_end(script, from)
156
+ last_change = from
157
+ (from...[script.size, from + 6].min).each do |i|
158
+ last_change = i if script[i][0] != :eq
159
+ end
160
+ last_change
161
+ end
162
+
163
+ def stats(hunks)
164
+ additions = hunks.sum { |h| h["b_lines"].count { |l| l["type"] == "addition" } }
165
+ deletions = hunks.sum { |h| h["a_lines"].count { |l| l["type"] == "deletion" } }
166
+ { "additions" => additions, "deletions" => deletions, "hunks" => hunks.size }
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,54 @@
1
+ module Textus
2
+ module Protocol
3
+ module EntryConstraint
4
+ module_function
5
+
6
+ def validate_naming!(key, mentry)
7
+ naming = mentry.naming
8
+ return unless naming
9
+ return unless key.start_with?("#{mentry.key}.")
10
+
11
+ last = key.split(".").last
12
+
13
+ case naming
14
+ when "sequential"
15
+ return if last.match?(/\A\d{4}-[a-z][a-z0-9-]*\z/)
16
+
17
+ raise UsageError.new(
18
+ "naming violation: '#{key}' last segment must match NNNN-slug " \
19
+ "(entry '#{mentry.key}' has naming: sequential)",
20
+ )
21
+ when "dated"
22
+ return if last.match?(/\A\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*\z/)
23
+
24
+ raise UsageError.new(
25
+ "naming violation: '#{key}' last segment must match YYYY-MM-DD-slug " \
26
+ "(entry '#{mentry.key}' has naming: dated)",
27
+ )
28
+ end
29
+ end
30
+
31
+ def validate_schema!(payload, mentry, schemas:)
32
+ schema_name = mentry.schema
33
+ return unless schema_name && schemas
34
+
35
+ schema = schemas.fetch_or_nil(schema_name)
36
+ return unless schema
37
+ return if mentry.lane == "scratchpad"
38
+
39
+ schema.validate!(payload.meta || {})
40
+ rescue Textus::SchemaViolation
41
+ raise if mentry.lane != "scratchpad"
42
+ end
43
+
44
+ def validate_format!(payload, mentry, format: nil)
45
+ fmt_name = format || mentry.format
46
+ fmt = Textus::Protocol::Format.for(fmt_name)
47
+ fmt.validate_raw_entry!(
48
+ { "_meta" => payload.meta, "content" => payload.content },
49
+ mentry.lane,
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
1
+ require "dry-struct"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Envelope < Dry::Struct
6
+ attribute :protocol, Textus::Value::Types::String
7
+ attribute :key, Textus::Value::Types::String
8
+ attribute :lane, Textus::Value::Types::String
9
+ attribute :path, Textus::Value::Types::String
10
+ attribute :format, Textus::Value::Types::FormatName
11
+ attribute :etag, Textus::Value::Types::String
12
+ attribute :uid, Textus::Value::Types::String.optional
13
+ attribute :sources, Textus::Value::Types::Array.of(Textus::Value::Types::Any).optional
14
+ attribute :schema_ref, Textus::Value::Types::String.optional
15
+ attribute :meta, Textus::Value::Types::Hash.default({}.freeze)
16
+ attribute :body, Textus::Value::Types::String.optional
17
+ attribute :content, Textus::Value::Types::Any.optional
18
+ attribute :freshness, Textus::Value::Types::Any.optional
19
+
20
+ def self.build(key:, mentry:, path:, meta:, body:, etag:, content: nil, freshness: nil)
21
+ new(
22
+ protocol: Textus::PROTOCOL,
23
+ key:,
24
+ lane: mentry.lane,
25
+ path:,
26
+ format: mentry.format,
27
+ uid: extract_uid(meta),
28
+ sources: extract_sources(meta),
29
+ etag:,
30
+ schema_ref: mentry.schema,
31
+ meta: meta || {},
32
+ body:,
33
+ content:,
34
+ freshness:,
35
+ )
36
+ end
37
+
38
+ def with(**attrs) = self.class.new(to_h.merge(attrs))
39
+
40
+ def to_h_for_wire
41
+ h = {
42
+ "protocol" => protocol, "key" => key, "lane" => lane,
43
+ "path" => path, "format" => format, "_meta" => meta,
44
+ "body" => body, "etag" => etag, "schema_ref" => schema_ref, "uid" => uid
45
+ }
46
+ h["sources"] = sources if sources
47
+ h["content"] = content unless content.nil?
48
+ freshness&.to_h_for_wire&.each { |k, v| h[k] = v }
49
+ h
50
+ end
51
+
52
+ def stale? = freshness.is_a?(Hash) && freshness["stale"] == true
53
+
54
+ def fetching? = freshness.is_a?(Hash) && freshness["fetching"] == true
55
+
56
+ def self.extract_uid(meta)
57
+ v = meta.is_a?(Hash) ? meta["uid"] : nil
58
+ v.is_a?(String) ? v : nil
59
+ end
60
+
61
+ def self.extract_sources(meta)
62
+ v = meta.is_a?(Hash) ? meta["sources"] : nil
63
+ return nil unless v.is_a?(Array) && !v.empty?
64
+
65
+ valid = v.select { |s| s.is_a?(String) || (s.is_a?(Hash) && s["key"].is_a?(String)) }
66
+ valid.empty? ? nil : valid
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Base
5
+ def self.parse(_raw, path: nil)
6
+ _ = path
7
+ raise NotImplementedError.new("#{name}.parse not implemented")
8
+ end
9
+
10
+ def self.serialize(meta: {}, body: "", content: nil)
11
+ _ = meta
12
+ _ = body
13
+ _ = content
14
+ raise NotImplementedError.new("#{name}.serialize not implemented")
15
+ end
16
+
17
+ def self.extensions
18
+ raise NotImplementedError.new("#{name}.extensions not implemented")
19
+ end
20
+
21
+ def self.validate_against(schema, parsed)
22
+ schema.validate!(parsed["_meta"] || {})
23
+ end
24
+
25
+ def self.nested_glob
26
+ raise NotImplementedError.new("#{name}.nested_glob not implemented")
27
+ end
28
+
29
+ def self.validate_path_extension(path, nested)
30
+ ext = File.extname(path)
31
+ if nested
32
+ return if ext == ""
33
+
34
+ raise UsageError.new("#{format_name} nested path must not have an extension")
35
+ end
36
+
37
+ return if extensions.include?(ext)
38
+
39
+ raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
40
+ end
41
+
42
+ def self.enforce_name_match!(path, meta)
43
+ return unless meta.is_a?(Hash) && meta["name"]
44
+
45
+ ext = extensions.first
46
+ basename = File.basename(path, ext)
47
+ return if meta["name"] == basename
48
+
49
+ raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
50
+ end
51
+
52
+ def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
53
+ raw = File.binread(path)
54
+ parsed = parse(raw, path: path)
55
+ meta = parsed["_meta"] || {}
56
+ return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
57
+
58
+ new_meta = meta.merge("name" => basename)
59
+ File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
60
+ true
61
+ end
62
+
63
+ def self.format_name
64
+ name.split("::").last.downcase
65
+ end
66
+
67
+ def self.validate_raw_entry!(_parsed, _lane); end
68
+
69
+ def self.nested_ext(_segment = nil) = extensions.first
70
+
71
+ def self.serialize_for_put(meta:, body:, content:, path:)
72
+ _ = path
73
+ _ = content
74
+ bytes = serialize(meta: meta || {}, body: body.to_s)
75
+ [bytes, meta, body.to_s, nil]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Bash < Script
5
+ def self.extensions = [".sh"]
6
+ def self.nested_ext(*) = ".sh"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Javascript < Script
5
+ def self.extensions = [".js"]
6
+ def self.nested_ext(*) = ".js"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ require "json"
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Format
6
+ class Json < Base
7
+ def self.parse(raw, path: nil)
8
+ raw = raw.dup.force_encoding(Encoding::UTF_8)
9
+ raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
10
+
11
+ begin
12
+ parsed = ::JSON.parse(raw)
13
+ rescue ::JSON::ParserError => e
14
+ raise BadFrontmatter.new(path, "JSON parse failed: #{e.message}")
15
+ end
16
+ raise BadFrontmatter.new(path, "JSON top-level must be an object") unless parsed.is_a?(Hash)
17
+
18
+ meta = parsed["_meta"]
19
+ fm = meta.is_a?(Hash) ? meta : {}
20
+ content_without_meta = parsed.except("_meta")
21
+ { "_meta" => fm, "body" => raw, "content" => content_without_meta }
22
+ end
23
+
24
+ def self.serialize(meta:, body:, content: nil)
25
+ if content.is_a?(Hash)
26
+ on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
27
+ out = ::JSON.pretty_generate(on_disk)
28
+ out += "\n" unless out.end_with?("\n")
29
+ out
30
+ elsif body && !body.to_s.empty?
31
+ b = body.to_s
32
+ b += "\n" unless b.end_with?("\n")
33
+ b
34
+ else
35
+ raise UsageError.new("json serialize requires :content or :body")
36
+ end
37
+ end
38
+
39
+ def self.validate_against(schema, parsed)
40
+ schema.validate!(parsed["content"] || {})
41
+ end
42
+
43
+ def self.extensions = [".json"]
44
+ def self.nested_glob = "**/*.json"
45
+
46
+ def self.serialize_for_put(meta:, body:, content:, path:)
47
+ raise UsageError.new("put for json requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
48
+
49
+ if content.nil?
50
+ begin
51
+ parsed = parse(body.to_s, path: path)
52
+ rescue BadFrontmatter => e
53
+ raise BadContent.new(path, "bad_content: #{e.message}")
54
+ end
55
+ [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
56
+ else
57
+ bytes = serialize(meta: meta, body: "", content: content)
58
+ [bytes, meta, bytes, content]
59
+ end
60
+ end
61
+
62
+ def self.validate_path_extension(path, nested)
63
+ ext = File.extname(path)
64
+ if nested
65
+ return if ext == ""
66
+
67
+ raise UsageError.new("nested json path must not have an extension")
68
+ end
69
+
70
+ return if ext == ".json"
71
+
72
+ raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
73
+ end
74
+
75
+ def self.data_to_payload(data)
76
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
77
+ { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ require "yaml"
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Format
6
+ class Markdown < Base
7
+ def self.parse(raw, path: nil)
8
+ raw = raw.dup.force_encoding(Encoding::UTF_8)
9
+ raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
10
+ return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
11
+
12
+ lines = raw.split(/\r?\n/, -1)
13
+ close_idx = lines[1..].index("---")
14
+ raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
15
+
16
+ close_idx += 1
17
+ fm_yaml = lines[1...close_idx].join("\n")
18
+ body = lines[(close_idx + 1)..].join("\n")
19
+ begin
20
+ fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
21
+ rescue Psych::SyntaxError => e
22
+ raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
23
+ end
24
+ fm = {} unless fm.is_a?(Hash)
25
+ { "_meta" => fm, "body" => body, "content" => nil }
26
+ end
27
+
28
+ def self.serialize(meta:, body:, content: nil)
29
+ _ = content
30
+ fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
31
+ body = body.to_s
32
+ body += "\n" unless body.empty? || body.end_with?("\n")
33
+ "---\n#{fm_yaml}---\n#{body}"
34
+ end
35
+
36
+ def self.extensions = [".md"]
37
+ def self.nested_glob = "**/*.md"
38
+
39
+ def self.validate_path_extension(path, _nested)
40
+ ext = File.extname(path)
41
+ return if ["", ".md"].include?(ext)
42
+
43
+ raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
44
+ end
45
+
46
+ def self.data_to_payload(data)
47
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
48
+ { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Python < Script
5
+ def self.extensions = [".py"]
6
+ def self.nested_ext(*) = ".py"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Ruby < Script
5
+ def self.extensions = [".rb"]
6
+ def self.nested_ext(*) = ".rb"
7
+ end
8
+ end
9
+ end
10
+ end