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,43 @@
1
+ module Textus
2
+ module Protocol
3
+ module Produce
4
+ class Render
5
+ class Context
6
+ def self.for(locals:, resolver: nil, from_path: nil, from_key: nil, edge_store: nil)
7
+ new(locals, resolver, from_path, from_key, edge_store)
8
+ end
9
+
10
+ def initialize(locals, resolver, from_path, from_key, edge_store)
11
+ @locals = locals
12
+ @resolver = resolver
13
+ @from_path = from_path
14
+ @from_key = from_key
15
+ @edge_store = edge_store
16
+ end
17
+
18
+ def to_erb_binding
19
+ mod = Module.new
20
+ @locals.each { |k, v| mod.define_method(k.to_sym) { v } }
21
+ obj = Object.new.extend(mod)
22
+
23
+ if @resolver && @from_path
24
+ resolver = @resolver
25
+ from_path = @from_path
26
+ from_key = @from_key
27
+ edge_store = @edge_store
28
+ obj.define_singleton_method(:textus_link) do |key|
29
+ resolved = resolver.resolve(key: key, from_path: from_path)
30
+ edge_store&.record(from_key: from_key, to_key: key)
31
+ resolved
32
+ rescue Textus::Protocol::Links::Resolver::UnknownKeyError
33
+ "`textus get #{key}`"
34
+ end
35
+ end
36
+
37
+ obj.instance_eval { binding }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ module Textus
2
+ module Protocol
3
+ module Produce
4
+ class Render
5
+ def initialize(template_loader:, manifest: nil, source_publish_path: nil, entry_key: nil, edge_store: nil)
6
+ @template_loader = template_loader
7
+ @manifest = manifest
8
+ @source_publish_path = source_publish_path
9
+ @entry_key = entry_key
10
+ @edge_store = edge_store
11
+ end
12
+
13
+ def bytes_for(target:, data:, boot:)
14
+ raise ArgumentError.new("Produce::Render called for a verbatim target #{target.to.inspect}") unless target.renders?
15
+
16
+ locals = target.inject_boot ? data.merge("boot" => boot) : data
17
+ resolver = @manifest ? Textus::Protocol::Links::Resolver.new(manifest: @manifest) : nil
18
+ ctx = Render::Context.for(
19
+ locals: locals,
20
+ resolver: resolver,
21
+ from_path: target.to,
22
+ from_key: @entry_key,
23
+ edge_store: @edge_store,
24
+ )
25
+ raw = ERB.new(@template_loader.call(target.template), trim_mode: "-").result(ctx.to_erb_binding)
26
+ rewrite(raw, target.to, resolver)
27
+ end
28
+
29
+ private
30
+
31
+ def rewrite(bytes, from_path, resolver)
32
+ return bytes unless resolver && bytes.include?("textus:")
33
+
34
+ Textus::Protocol::Links::UriRewriter.new(
35
+ resolver: resolver,
36
+ from_path: from_path,
37
+ from_key: @entry_key,
38
+ edge_store: @edge_store,
39
+ ).rewrite(bytes)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ module Textus
2
+ module Protocol
3
+ class Schema
4
+ class Registry
5
+ def initialize(dir)
6
+ @dir = dir
7
+ @schemas = {}
8
+ load_all
9
+ end
10
+
11
+ def fetch(name)
12
+ @schemas[name] || raise(Textus::IoError.new("schema not found: #{File.join(@dir, "#{name}.yaml")}"))
13
+ end
14
+
15
+ def fetch_or_nil(name)
16
+ return nil if name.nil?
17
+
18
+ fetch(name)
19
+ end
20
+
21
+ def all
22
+ @schemas.values
23
+ end
24
+
25
+ def by_name
26
+ @schemas.dup
27
+ end
28
+
29
+ private
30
+
31
+ def load_all
32
+ return unless File.directory?(@dir)
33
+
34
+ Dir.glob(File.join(@dir, "**", "*.yaml")).each do |path|
35
+ name = path.delete_prefix("#{@dir}/").sub(/\.yaml\z/, "")
36
+ @schemas[name] = Textus::Protocol::Schema.load(path)
37
+ rescue StandardError => e
38
+ warn "textus: failed to load schema '#{name}' at #{path}: #{e.message}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,104 @@
1
+ require "yaml"
2
+ require "fileutils"
3
+
4
+ module Textus
5
+ module Protocol
6
+ class Schema
7
+ module Tools
8
+ def self.init(store, name:, from:)
9
+ env = pure_get(store, Textus::Value::Role::DEFAULT, from)
10
+ meta = env["_meta"]
11
+ schema = {
12
+ "name" => name,
13
+ "required" => meta.keys,
14
+ "optional" => [],
15
+ "fields" => meta.each_with_object({}) { |(k, v), h| h[k] = { "type" => infer_type(v) } },
16
+ }
17
+ geom = Textus::Protocol::Layout.new(store.root)
18
+ FileUtils.mkdir_p(geom.schemas_dir)
19
+ target = geom.schema_path(name)
20
+ File.write(target, YAML.dump(schema))
21
+ { "protocol" => PROTOCOL, "schema_name" => name, "path" => target }
22
+ end
23
+
24
+ def self.diff(store, name:)
25
+ schema = load_schema(store, name)
26
+ drift = []
27
+ store.manifest.resolver.enumerate.each do |row|
28
+ env = pure_get(store, Textus::Value::Role::DEFAULT, row[:key])
29
+ begin
30
+ schema.validate!(env["_meta"])
31
+ rescue SchemaViolation => e
32
+ drift << { "key" => row[:key], "details" => e.details }
33
+ end
34
+ end
35
+ { "protocol" => PROTOCOL, "schema_name" => name, "drift" => drift }
36
+ end
37
+
38
+ def self.migrate(store, name:, rename: nil)
39
+ renames =
40
+ if rename
41
+ old_field, new_field = rename.split(":", 2)
42
+ raise UsageError.new("--rename=OLD:NEW") unless old_field && new_field && !new_field.empty?
43
+
44
+ { old_field => new_field }
45
+ else
46
+ load_schema(store, name).evolution["migrate_from"] || {}
47
+ end
48
+ raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
49
+
50
+ authority = accept_role_for(store)
51
+ ops = store.with_role(authority)
52
+ touched = []
53
+ store.manifest.resolver.enumerate.each do |row|
54
+ env = pure_get(store, authority, row[:key])
55
+ meta = (env["_meta"] || {}).dup
56
+ changed = false
57
+ renames.each do |old, new|
58
+ if meta.key?(old)
59
+ meta[new] = meta.delete(old)
60
+ changed = true
61
+ end
62
+ end
63
+ next unless changed
64
+
65
+ ops.put(key: row[:key], meta: meta, body: env["body"])
66
+ touched << row[:key]
67
+ end
68
+ { "protocol" => PROTOCOL, "migrated" => touched, "renames" => renames }
69
+ end
70
+
71
+ def self.infer_type(value)
72
+ case value
73
+ when String then "string"
74
+ when Numeric then "number"
75
+ when true, false then "boolean"
76
+ when Array then "array"
77
+ when Hash then "object"
78
+ else "string"
79
+ end
80
+ end
81
+
82
+ def self.pure_get(store, role, key)
83
+ store.with_role(role).get(key: key)
84
+ end
85
+
86
+ def self.load_schema(store, name)
87
+ store.schemas.fetch(name)
88
+ rescue IoError
89
+ raise UsageError.new("schema not found: #{name}")
90
+ end
91
+
92
+ def self.accept_role_for(store)
93
+ authority = store.manifest.policy.writers_for("knowledge").first
94
+ return authority if authority
95
+
96
+ raise UsageError.new(
97
+ "schema migrate requires a role that can write knowledge; " \
98
+ "none found in the fixed lane table",
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -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