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,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
@@ -0,0 +1,55 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Script < Base
5
+ EXTENSIONS = %w[.rb .py .sh .js .pl .rs .exs].freeze
6
+
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
+ { "_meta" => {}, "body" => raw, "content" => nil }
12
+ end
13
+
14
+ def self.serialize(meta:, body:, content: nil)
15
+ _ = meta
16
+ _ = content
17
+ b = body.to_s
18
+ b += "\n" unless b.empty? || b.end_with?("\n")
19
+ b
20
+ end
21
+
22
+ def self.extensions = EXTENSIONS
23
+ def self.nested_glob = "**/*"
24
+ def self.nested_ext(*) = ""
25
+ def self.enforce_name_match!(_path, _meta); end
26
+
27
+ def self.rewrite_name(_path, _basename)
28
+ false
29
+ end
30
+
31
+ def self.validate_path_extension(path, nested)
32
+ ext = File.extname(path)
33
+ if nested
34
+ return if ext == ""
35
+
36
+ raise UsageError.new("nested script path must not have an extension")
37
+ end
38
+
39
+ return if EXTENSIONS.include?(ext) || ext == ""
40
+
41
+ raise UsageError.new("script format requires #{EXTENSIONS.join(" | ")} extension (got #{ext.inspect})")
42
+ end
43
+
44
+ def self.validate_raw_entry!(_parsed, lane)
45
+ nil unless lane == "scratchpad"
46
+ end
47
+
48
+ def self.data_to_payload(data)
49
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
50
+ { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ class Text < Base
5
+ def self.parse(raw, path: nil)
6
+ raw = raw.dup.force_encoding(Encoding::UTF_8)
7
+ raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
8
+
9
+ { "_meta" => {}, "body" => raw, "content" => nil }
10
+ end
11
+
12
+ def self.serialize(meta:, body:, content: nil)
13
+ _ = meta
14
+ _ = content
15
+ b = body.to_s
16
+ b += "\n" unless b.empty? || b.end_with?("\n")
17
+ b
18
+ end
19
+
20
+ def self.extensions = [".txt"]
21
+ def self.nested_glob = "**/*.txt"
22
+ def self.enforce_name_match!(_path, _meta); end
23
+
24
+ def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
25
+ false
26
+ end
27
+
28
+ def self.validate_path_extension(path, nested)
29
+ ext = File.extname(path)
30
+ if nested
31
+ return if ext == ""
32
+
33
+ raise UsageError.new("nested text path must not have an extension")
34
+ end
35
+
36
+ return if [".txt", ""].include?(ext)
37
+
38
+ raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
39
+ end
40
+
41
+ def self.data_to_payload(data)
42
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
43
+ { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,105 @@
1
+ require "yaml"
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Format
6
+ class Yaml < 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 = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
13
+ rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
14
+ raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
15
+ end
16
+ raise BadFrontmatter.new(path, "YAML top-level must be a mapping") 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
+ ::YAML.dump(on_disk).sub(/\A---\n/, "")
28
+ elsif body && !body.to_s.empty?
29
+ b = body.to_s
30
+ b += "\n" unless b.end_with?("\n")
31
+ b
32
+ else
33
+ raise UsageError.new("yaml serialize requires :content or :body")
34
+ end
35
+ end
36
+
37
+ def self.validate_against(schema, parsed)
38
+ schema.validate!(parsed["content"] || {})
39
+ end
40
+
41
+ RAW_REQUIRED = %w[ingested_at content_hash].freeze
42
+ RAW_SOURCE_KINDS = %w[url file asset].freeze
43
+
44
+ def self.validate_raw_entry!(parsed, lane)
45
+ return unless lane == "raw"
46
+
47
+ content = parsed["content"] || {}
48
+ return if content["superseded_by"]
49
+
50
+ missing = RAW_REQUIRED.reject { |f| content[f] }
51
+ raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
52
+
53
+ source = content["source"] || {}
54
+ kind = source["kind"]
55
+ unless RAW_SOURCE_KINDS.include?(kind)
56
+ raise Textus::BadContent.new(
57
+ nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
58
+ )
59
+ end
60
+
61
+ return unless kind == "url" && !source["url"]
62
+
63
+ raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
64
+ end
65
+
66
+ def self.extensions = [".yaml", ".yml"]
67
+ def self.nested_glob = "**/*.{yaml,yml}"
68
+
69
+ def self.serialize_for_put(meta:, body:, content:, path:)
70
+ raise UsageError.new("put for yaml requires content: or body:") if content.nil? && (body.nil? || body.to_s.empty?)
71
+
72
+ if content.nil?
73
+ begin
74
+ parsed = parse(body.to_s, path: path)
75
+ rescue BadFrontmatter => e
76
+ raise BadContent.new(path, "bad_content: #{e.message}")
77
+ end
78
+ [body.to_s, parsed["_meta"], body.to_s, parsed["content"]]
79
+ else
80
+ bytes = serialize(meta: meta, body: "", content: content)
81
+ [bytes, meta, bytes, content]
82
+ end
83
+ end
84
+
85
+ def self.validate_path_extension(path, nested)
86
+ ext = File.extname(path)
87
+ if nested
88
+ return if ext == ""
89
+
90
+ raise UsageError.new("nested yaml path must not have an extension")
91
+ end
92
+
93
+ return if [".yaml", ".yml"].include?(ext)
94
+
95
+ raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
96
+ end
97
+
98
+ def self.data_to_payload(data)
99
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
100
+ { meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,73 @@
1
+ module Textus
2
+ module Protocol
3
+ module Format
4
+ SEP = "---".freeze
5
+
6
+ STRATEGIES = {
7
+ "markdown" => -> { Protocol::Format::Markdown },
8
+ "json" => -> { Protocol::Format::Json },
9
+ "yaml" => -> { Protocol::Format::Yaml },
10
+ "text" => -> { Protocol::Format::Text },
11
+ "script" => -> { Protocol::Format::Script },
12
+ "bash" => -> { Protocol::Format::Bash },
13
+ "ruby" => -> { Protocol::Format::Ruby },
14
+ "python" => -> { Protocol::Format::Python },
15
+ "javascript" => -> { Protocol::Format::Javascript },
16
+ }.freeze
17
+
18
+ @registry = nil
19
+
20
+ def self.registry=(reg)
21
+ @registry = reg
22
+ end
23
+
24
+ def self.registry
25
+ @registry
26
+ end
27
+
28
+ EXT_TO_FORMAT = {
29
+ ".md" => "markdown",
30
+ ".json" => "json",
31
+ ".yaml" => "yaml",
32
+ ".yml" => "yaml",
33
+ ".txt" => "text",
34
+ ".rb" => "ruby",
35
+ ".py" => "python",
36
+ ".sh" => "bash",
37
+ ".js" => "javascript",
38
+ ".pl" => "script",
39
+ ".rs" => "script",
40
+ ".exs" => "script",
41
+ }.freeze
42
+
43
+ def self.for(format)
44
+ key = format.to_s
45
+ return registry.fetch(key).call if registry&.key?(key)
46
+
47
+ STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
48
+ end
49
+
50
+ def self.infer_from_extension(ext)
51
+ EXT_TO_FORMAT[ext]
52
+ end
53
+
54
+ def self.formats
55
+ EXT_TO_FORMAT.values.uniq
56
+ end
57
+
58
+ def self.parse(raw, path: nil, format: "markdown")
59
+ Protocol::Format.for(format).parse(raw, path: path)
60
+ end
61
+
62
+ def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
63
+ Protocol::Format.for(format).serialize(meta: meta, body: body, content: content)
64
+ end
65
+
66
+ def self.data_to_payload(data, format)
67
+ return { meta: {}, body: "", content: nil } if data.nil?
68
+
69
+ Protocol::Format.for(format).data_to_payload(data)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Textus
4
+ module Protocol
5
+ module Freshness
6
+ class TtlEvaluator
7
+ def initialize(manifest:, file_stat:, clock:)
8
+ @manifest = manifest
9
+ @file_stat = file_stat
10
+ @clock = clock
11
+ end
12
+
13
+ def verdict(mentry)
14
+ ttl = lane_ttl(mentry.lane)
15
+ return fresh if ttl.nil?
16
+
17
+ stale = age_stale?(file_basis(mentry), ttl)
18
+ Verdict.build(stale: stale, reason: stale ? "ttl exceeded" : nil, fetching: false)
19
+ end
20
+
21
+ def stale_keys(prefix: nil, lane: nil)
22
+ @manifest.data.entries.select { |m| due?(m, prefix: prefix, lane: lane) }.map(&:key)
23
+ end
24
+
25
+ private
26
+
27
+ def fresh = Verdict.build(stale: false, reason: nil, fetching: false)
28
+
29
+ def file_basis(mentry)
30
+ path = @manifest.resolver.resolve(mentry.key).path
31
+ return nil unless @file_stat.exists?(path)
32
+
33
+ @file_stat.mtime(path)
34
+ end
35
+
36
+ def lane_ttl(lane)
37
+ Textus::Protocol::V4::LANES.dig(lane, :retention_ttl)
38
+ end
39
+
40
+ def due?(mentry, prefix:, lane:)
41
+ return false if lane && mentry.lane != lane
42
+ return false if prefix && !mentry.key.start_with?(prefix)
43
+
44
+ ttl = lane_ttl(mentry.lane)
45
+ return false if ttl.nil?
46
+
47
+ path = @manifest.resolver.resolve(mentry.key).path
48
+ return true unless @file_stat.exists?(path)
49
+
50
+ age_stale?(file_basis(mentry), ttl)
51
+ end
52
+
53
+ def age_stale?(basis, ttl)
54
+ return true if basis.nil?
55
+
56
+ (@clock.now - basis).to_i > ttl
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ module Textus
2
+ module Protocol
3
+ class Gate
4
+ HANDLER_GROUPS = {
5
+ read: Handlers::Read,
6
+ write: Handlers::Write,
7
+ maintenance: Handlers::System,
8
+ }.freeze
9
+
10
+ def initialize(manifest:, resolver:, container:, store_engine:, policy: nil, async: nil)
11
+ @manifest = manifest
12
+ @resolver = resolver
13
+ @container = container
14
+ @store_engine = store_engine
15
+ @async = async
16
+ @policy = policy || manifest.policy
17
+ end
18
+
19
+ def dispatch(role, verb, params = {}, call: nil)
20
+ spec = VerbRegistry.for(verb)
21
+ return error(:unknown_verb, "Unknown verb: #{verb}") unless spec
22
+
23
+ key = params[:key]
24
+ authorize!(role, verb, key, spec)
25
+
26
+ call_obj = call || Value::Call.build(role:, correlation_id: SecureRandom.uuid)
27
+ bound = Protocol::Binder.bind(spec, params)
28
+ ctx_class = spec.read? && !spec.system? ? Handlers::QueryContext : Handlers::CommandContext
29
+ ctx = ctx_class.new(@store_engine, @container, call_obj)
30
+
31
+ handler = spec.lane ? Lane.handler_for(spec.lane) : handler_group(spec)
32
+ handler.public_send(verb, **bound, ctx:, call: call_obj)
33
+ end
34
+
35
+ private
36
+
37
+ def handler_group(spec)
38
+ return Handlers::System if spec.system?
39
+
40
+ HANDLER_GROUPS[spec.category] || Handlers::System
41
+ end
42
+
43
+ def authorize!(role, _verb, key, spec)
44
+ return true unless spec&.write?
45
+
46
+ key_lane = key&.split(".")&.first
47
+ lane = key_lane || spec.lane&.to_s
48
+ return true unless lane
49
+
50
+ lane_map = Textus::Protocol::V4::LANES[lane]
51
+ return true unless lane_map
52
+
53
+ @policy.allowed!(role:, lane:, key:)
54
+ end
55
+
56
+ def error(code, message)
57
+ { error: { code: code.to_s, message: } }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ module Handlers
4
+ class CommandContext < QueryContext
5
+ # StoreEngine delegation (write side)
6
+ def put(...) = @store_engine.put(...)
7
+ def delete(...) = @store_engine.delete(...)
8
+ def move(...) = @store_engine.move(...)
9
+
10
+ # Infrastructure (ops, drain, pulse)
11
+ def audit_log = @container.store.audit_log
12
+ def workflow_queue = @container.store.workflow_queue
13
+ def file_system = @container.store.file_system
14
+ def workflows = @container.workflows
15
+ def workflow_registry = @container.workflow_registry
16
+ end
17
+ end
18
+ end
19
+ end