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
@@ -1,59 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Format
5
- class Markdown < Base
6
- def self.parse(raw, path: nil)
7
- raw = raw.dup.force_encoding(Encoding::UTF_8)
8
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
9
- return { "_meta" => {}, "body" => raw, "content" => nil } unless raw.start_with?("---\n") || raw.start_with?("---\r\n")
10
-
11
- lines = raw.split(/\r?\n/, -1)
12
- close_idx = lines[1..].index("---")
13
- raise BadFrontmatter.new(path, "frontmatter not terminated") unless close_idx
14
-
15
- close_idx += 1
16
- fm_yaml = lines[1...close_idx].join("\n")
17
- body = lines[(close_idx + 1)..].join("\n")
18
- begin
19
- fm = fm_yaml.strip.empty? ? {} : ::YAML.safe_load(fm_yaml, permitted_classes: [Date, Time], aliases: false)
20
- rescue Psych::SyntaxError => e
21
- raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
22
- end
23
- fm = {} unless fm.is_a?(Hash)
24
- { "_meta" => fm, "body" => body, "content" => nil }
25
- end
26
-
27
- def self.serialize(meta:, body:, content: nil)
28
- _ = content
29
- fm_yaml = meta.empty? ? "" : ::YAML.dump(meta).sub(/\A---\n/, "")
30
- body = body.to_s
31
- body += "\n" unless body.empty? || body.end_with?("\n")
32
- "---\n#{fm_yaml}---\n#{body}"
33
- end
34
-
35
- def self.extensions = [".md"]
36
-
37
- def self.nested_glob = "**/*.md"
38
-
39
- def self.serialize_for_put(meta:, body:, content:, path:)
40
- _ = path
41
- _ = content
42
- bytes = serialize(meta: meta || {}, body: body.to_s)
43
- [bytes, meta, body.to_s, nil]
44
- end
45
-
46
- def self.validate_path_extension(path, _nested)
47
- ext = File.extname(path)
48
- return if ["", ".md"].include?(ext)
49
-
50
- raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
51
- end
52
-
53
- def self.data_to_payload(data)
54
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
55
- { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
56
- end
57
- end
58
- end
59
- end
@@ -1,55 +0,0 @@
1
- module Textus
2
- module Format
3
- class Text < Base
4
- def self.parse(raw, path: nil)
5
- raw = raw.dup.force_encoding(Encoding::UTF_8)
6
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
7
-
8
- { "_meta" => {}, "body" => raw, "content" => nil }
9
- end
10
-
11
- def self.serialize(meta:, body:, content: nil)
12
- _ = meta
13
- _ = content
14
- b = body.to_s
15
- b += "\n" unless b.empty? || b.end_with?("\n")
16
- b
17
- end
18
-
19
- def self.extensions = [".txt"]
20
-
21
- def self.nested_glob = "**/*.txt"
22
-
23
- def self.enforce_name_match!(_path, _meta); end
24
-
25
- def self.serialize_for_put(meta:, body:, content:, path:)
26
- _ = path
27
- _ = content
28
- bytes = serialize(meta: meta || {}, body: body.to_s)
29
- [bytes, meta, body.to_s, nil]
30
- end
31
-
32
- def self.rewrite_name(_path, _basename) # rubocop:disable Naming/PredicateMethod
33
- false
34
- end
35
-
36
- def self.validate_path_extension(path, nested)
37
- ext = File.extname(path)
38
- if nested
39
- return if ext == ""
40
-
41
- raise UsageError.new("nested text path must not have an extension")
42
- end
43
-
44
- return if [".txt", ""].include?(ext)
45
-
46
- raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
47
- end
48
-
49
- def self.data_to_payload(data)
50
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
51
- { meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
52
- end
53
- end
54
- end
55
- end
@@ -1,104 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Format
5
- class Yaml < Base
6
- def self.parse(raw, path: nil)
7
- raw = raw.dup.force_encoding(Encoding::UTF_8)
8
- raise BadFrontmatter.new(path, "entry is not valid UTF-8") unless raw.valid_encoding?
9
-
10
- begin
11
- parsed = ::YAML.safe_load(raw, permitted_classes: [Date, Time], aliases: false)
12
- rescue Psych::SyntaxError, Psych::AliasesNotEnabled, Psych::DisallowedClass => e
13
- raise BadFrontmatter.new(path, "YAML parse failed: #{e.message}")
14
- end
15
- raise BadFrontmatter.new(path, "YAML top-level must be a mapping") unless parsed.is_a?(Hash)
16
-
17
- meta = parsed["_meta"]
18
- fm = meta.is_a?(Hash) ? meta : {}
19
- content_without_meta = parsed.except("_meta")
20
- { "_meta" => fm, "body" => raw, "content" => content_without_meta }
21
- end
22
-
23
- def self.serialize(meta:, body:, content: nil)
24
- if content.is_a?(Hash)
25
- on_disk = meta && !meta.empty? ? { "_meta" => meta }.merge(content) : content
26
- ::YAML.dump(on_disk).sub(/\A---\n/, "")
27
- elsif body && !body.to_s.empty?
28
- b = body.to_s
29
- b += "\n" unless b.end_with?("\n")
30
- b
31
- else
32
- raise UsageError.new("yaml serialize requires :content or :body")
33
- end
34
- end
35
-
36
- def self.validate_against(schema, parsed)
37
- schema.validate!(parsed["content"] || {})
38
- end
39
-
40
- RAW_REQUIRED = %w[ingested_at content_hash].freeze
41
- RAW_SOURCE_KINDS = %w[url file asset].freeze
42
-
43
- def self.validate_raw_entry!(parsed, lane)
44
- return unless lane == "raw"
45
-
46
- content = parsed["content"] || {}
47
- return if content["superseded_by"]
48
-
49
- missing = RAW_REQUIRED.reject { |f| content[f] }
50
- raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
51
-
52
- source = content["source"] || {}
53
- kind = source["kind"]
54
- unless RAW_SOURCE_KINDS.include?(kind)
55
- raise Textus::BadContent.new(
56
- nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
57
- )
58
- end
59
-
60
- return unless kind == "url" && !source["url"]
61
-
62
- raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
63
- end
64
-
65
- def self.extensions = [".yaml", ".yml"]
66
-
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
data/lib/textus/format.rb DELETED
@@ -1,61 +0,0 @@
1
- module Textus
2
- module Format
3
- SEP = "---".freeze
4
-
5
- STRATEGIES = {
6
- "markdown" => -> { Format::Markdown },
7
- "json" => -> { Format::Json },
8
- "yaml" => -> { Format::Yaml },
9
- "text" => -> { Format::Text },
10
- }.freeze
11
-
12
- # Optional registry for injectable format strategies. Tests or app
13
- # initializers can set Format.registry = { "custom" => -> { MyFormat }}
14
- @registry = nil
15
-
16
- def self.registry=(reg)
17
- @registry = reg
18
- end
19
-
20
- def self.registry
21
- @registry
22
- end
23
-
24
- EXT_TO_FORMAT = {
25
- ".md" => "markdown",
26
- ".json" => "json",
27
- ".yaml" => "yaml",
28
- ".yml" => "yaml",
29
- ".txt" => "text",
30
- }.freeze
31
-
32
- def self.for(format)
33
- key = format.to_s
34
- return registry.fetch(key).call if registry&.key?(key)
35
-
36
- STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
37
- end
38
-
39
- def self.infer_from_extension(ext)
40
- EXT_TO_FORMAT[ext]
41
- end
42
-
43
- def self.formats
44
- EXT_TO_FORMAT.values.uniq
45
- end
46
-
47
- def self.parse(raw, path: nil, format: "markdown")
48
- Format.for(format).parse(raw, path: path)
49
- end
50
-
51
- def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
52
- Format.for(format).serialize(meta: meta, body: body, content: content)
53
- end
54
-
55
- def self.data_to_payload(data, format)
56
- return { meta: {}, body: "", content: nil } if data.nil?
57
-
58
- Format.for(format).data_to_payload(data)
59
- end
60
- end
61
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class BootStore
5
- def initialize(container:)
6
- @container = container
7
- end
8
-
9
- def call(_command, _call)
10
- Value::Result.success(Textus::Boot.build(container: @container))
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,15 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class DoctorStore
5
- def initialize(container:)
6
- @container = container
7
- end
8
-
9
- def call(command, call)
10
- Value::Result.success(Textus::Doctor.build(container: @container, checks: command.checks, role: call.role))
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class DrainStore
5
- def initialize(container:, job_store:)
6
- @container = container
7
- @job_store = job_store
8
- end
9
-
10
- def call(_command, call)
11
- queue = Textus::Store::Jobs::Queue.new(store: @job_store)
12
- Textus::Store::Jobs::Planner.seed(container: @container, queue: queue, role: call.role)
13
- queue.reclaim(now: Textus::Port::Clock.new.now)
14
- summary = Textus::Store::Jobs::Worker.for(container: @container, queue: queue).drain
15
- Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => summary.failed.zero?,
16
- "completed" => summary.completed, "failed" => summary.failed)
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,159 +0,0 @@
1
- require "fileutils"
2
- require "date"
3
- require "digest"
4
-
5
- module Textus
6
- module Handlers
7
- module Maintenance
8
- class IngestEntry
9
- SOURCE_KINDS = %w[url file asset].freeze
10
- CONTENT_HASH_ALGO = "sha256"
11
-
12
- def initialize(container:)
13
- @container = container
14
- end
15
-
16
- def call(command, call)
17
- unless SOURCE_KINDS.include?(command.kind)
18
- return Value::Result.failure(:usage_error,
19
- "ingest kind must be one of #{SOURCE_KINDS.join("|")}")
20
- end
21
-
22
- case command.kind
23
- when "url" then return Value::Result.failure(:usage_error, "ingest url requires url") unless command.url
24
- when "file" then return Value::Result.failure(:usage_error, "ingest file requires path") unless command.path
25
- when "asset"
26
- return Value::Result.failure(:usage_error, "ingest asset requires path") unless command.path
27
- return Value::Result.failure(:usage_error, "ingest asset requires lane") unless command.lane
28
- end
29
-
30
- now = Time.now.utc
31
- key = derive_key(now, command.kind, command.slug)
32
- content_hash = compute_content_hash(command)
33
- mentry = @container.manifest.resolver.resolve(key).entry
34
- ts = now.iso8601
35
-
36
- structured = build_structured(ts, now, content_hash, command)
37
- store = @container.job_store
38
- index = Textus::Store::Index::Lookup.new(store:)
39
-
40
- duplicate_key = find_duplicate(index, content_hash, command)
41
-
42
- env = if duplicate_key && duplicate_key != key
43
- supersede_entry(duplicate_key, key, structured, call, store, command)
44
- else
45
- write_entry(key, structured, mentry, call)
46
- end
47
-
48
- rebuild_index(store)
49
- Value::Result.success(env)
50
- end
51
-
52
- private
53
-
54
- def derive_key(now, kind, slug)
55
- date = now.strftime("%Y.%m.%d")
56
- "raw.#{date}.#{kind}-#{slug}"
57
- end
58
-
59
- def compute_content_hash(command)
60
- digest = Digest::SHA256.new
61
- case command.kind
62
- when "url" then digest.update(command.url)
63
- when "file", "asset" then digest.file(command.path)
64
- end
65
- "#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
66
- end
67
-
68
- def build_structured(timestamp, now, content_hash, command)
69
- base = { "ingested_at" => timestamp, "content_hash" => content_hash }
70
- case command.kind
71
- when "url"
72
- base.merge("source" => { "kind" => "url", "url" => command.url,
73
- "label" => command.label || command.url }, "body" => nil)
74
- when "file"
75
- base.merge("source" => { "kind" => "file", "path" => command.path,
76
- "label" => command.label || File.basename(command.path) },
77
- "body" => File.read(command.path))
78
- when "asset"
79
- asset_rel = copy_asset(now, command.path, command.lane)
80
- base.merge("source" => { "kind" => "asset",
81
- "label" => command.label || File.basename(command.path) },
82
- "asset" => asset_rel, "body" => nil)
83
- end
84
- end
85
-
86
- def copy_asset(now, path, lane)
87
- date_path = now.strftime("%Y/%m/%d")
88
- filename = File.basename(path)
89
- assets_dir = @container.layout.asset_raw_dir(date_path, lane)
90
- FileUtils.mkdir_p(assets_dir)
91
- FileUtils.cp(path, File.join(assets_dir, filename))
92
- sentinel = @container.layout.asset_sentinel_path
93
- File.write(sentinel, "*\n") unless File.exist?(sentinel)
94
- "raw/#{date_path}/#{lane}/#{filename}"
95
- end
96
-
97
- def write_entry(key, structured, mentry, call)
98
- writer = Store::Entry::Writer.from(container: @container, call: call)
99
- writer.put(key, mentry: mentry,
100
- payload: Textus::Value::Payload.new(meta: nil, body: nil, content: structured))
101
- end
102
-
103
- def find_duplicate(index, content_hash, command)
104
- dup = index.find_by_hash(content_hash)
105
- return dup if dup
106
- return unless command.kind == "url"
107
-
108
- index.find_by_url(command.url)
109
- end
110
-
111
- def supersede_entry(old_key, new_key, structured, call, store, command)
112
- old_mentry = @container.manifest.resolver.resolve(old_key).entry
113
- reader = Store::Entry::Reader.from(container: @container)
114
- old_env = reader.read(old_key)
115
- old_content = old_env&.content || {}
116
- tombstone = {}
117
- %w[ingested_at].each { |k| tombstone[k] = old_content[k] if old_content.key?(k) }
118
- source_kind = old_content.dig("source", "kind")
119
- tombstone["source"] = { "kind" => source_kind } if source_kind
120
- tombstone["superseded_by"] = new_key
121
-
122
- writer = Store::Entry::Writer.from(container: @container, call: call)
123
- writer.put(old_key, mentry: old_mentry,
124
- payload: Textus::Value::Payload.new(meta: nil, body: nil, content: tombstone))
125
-
126
- structured["supersedes"] = old_key
127
- env = write_entry(new_key, structured,
128
- @container.manifest.resolver.resolve(new_key).entry, call)
129
-
130
- move_asset(old_content["asset"], command.lane) if command.kind == "asset" && old_content["asset"]
131
-
132
- rebuild_index(store)
133
- env
134
- end
135
-
136
- def move_asset(old_rel, lane)
137
- old_path = @container.layout.asset_resolve(old_rel)
138
- return unless File.exist?(old_path)
139
-
140
- now = Time.now.utc
141
- date_path = now.strftime("%Y/%m/%d")
142
- filename = File.basename(old_path)
143
- new_dir = @container.layout.asset_raw_dir(date_path, lane)
144
- new_path = File.join(new_dir, filename)
145
- return if old_path == new_path
146
-
147
- FileUtils.mkdir_p(new_dir)
148
- FileUtils.mv(old_path, new_path)
149
- rescue Errno::ENOENT, Errno::EACCES => e
150
- warn "[textus ingest] could not move asset #{old_rel}: #{e.message}"
151
- end
152
-
153
- def rebuild_index(store)
154
- Textus::Store::Index::Builder.new(store:).rebuild!(resolver: @container.manifest.resolver)
155
- end
156
- end
157
- end
158
- end
159
- end
@@ -1,21 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class JobsAction
5
- def initialize(job_store:)
6
- @job_store = job_store
7
- end
8
-
9
- def call(command, _call)
10
- queue = Textus::Store::Jobs::Queue.new(store: @job_store)
11
- case command.action
12
- when "retry" then queue.retry_failed(command.job_id)
13
- when "purge" then queue.purge(command.state)
14
- end
15
- Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => true,
16
- "state" => command.state, "jobs" => queue.list(command.state))
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class PublishedEntries
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(_command, _call)
10
- Value::Result.success(@manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
11
- { "key" => entry.key, "publish_to" => entry.publish_to }
12
- end)
13
- end
14
- end
15
- end
16
- end
17
- end
@@ -1,77 +0,0 @@
1
- module Textus
2
- module Handlers
3
- module Maintenance
4
- class RuleExplain
5
- def initialize(manifest:)
6
- @manifest = manifest
7
- end
8
-
9
- def call(command, _call)
10
- key = command.key
11
- result = if command.detail
12
- explain(key)
13
- else
14
- effective(key)
15
- end
16
- Value::Result.success(result)
17
- end
18
-
19
- LEAN_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY
20
- .select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
21
- DETAIL_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY
22
- .select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
23
- EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| Textus::Manifest::Schema::FIELD_REGISTRY[f][:policy_class] }.freeze
24
-
25
- private
26
-
27
- def effective(key)
28
- set = @manifest.rules.for(key)
29
- LEAN_FIELDS.each_with_object({}) do |field, out|
30
- value = set.public_send(field)
31
- out[field.to_s] = lean_value(field, value) unless value.nil?
32
- end
33
- end
34
-
35
- def lean_value(field, value)
36
- case field
37
- when :retention then retention_hash(value, string_keys: true)
38
- when :react then value.to_h
39
- else value
40
- end
41
- end
42
-
43
- def explain(key)
44
- matching = @manifest.rules.explain(key)
45
- winners = @manifest.rules.for(key)
46
- {
47
- key: key,
48
- matched_blocks: matching.map do |block|
49
- { match: block.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !block.public_send(f).nil?] })
50
- end,
51
- effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
52
- guards: Textus::Manifest::Policy::Predicates::FLOOR.keys.to_h do |action|
53
- floor = Textus::Manifest::Policy::Predicates::FLOOR.fetch(action, [])
54
- rule = Array(@manifest.rules.for(key).guard&.dig(action.to_s))
55
- [action, { floor: floor, rule: rule }]
56
- end,
57
- }
58
- end
59
-
60
- def effective_value(field, value)
61
- return nil if value.nil?
62
-
63
- case field
64
- when :retention then retention_hash(value, string_keys: false)
65
- when :react then value.to_h
66
- else value
67
- end
68
- end
69
-
70
- def retention_hash(retention, string_keys:)
71
- h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
72
- string_keys ? h.transform_keys(&:to_s) : h
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,54 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- module Handlers
5
- module Maintenance
6
- class RuleLint
7
- def initialize(manifest:)
8
- @manifest = manifest
9
- end
10
-
11
- def call(command, _call)
12
- root = @manifest.data.root
13
- live_rules = current_rules(root)
14
- candidate_result = parse_candidate(command.candidate_yaml)
15
- return candidate_result if candidate_result.is_a?(Value::Result) && candidate_result.failure?
16
-
17
- candidate_rules = candidate_result
18
- live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
19
- candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
20
-
21
- steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
22
- { "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
23
- end
24
- (live_by_match.keys - candidate_by_match.keys).each do |match|
25
- steps << { "op" => "remove_rule", "match" => match }
26
- end
27
- (live_by_match.keys & candidate_by_match.keys).each do |match|
28
- next if live_by_match[match] == candidate_by_match[match]
29
-
30
- steps << { "op" => "change_rule", "match" => match, "from" => live_by_match[match], "to" => candidate_by_match[match] }
31
- end
32
-
33
- Value::Result.success(Textus::Store::Jobs::Plan.new(steps: steps, warnings: []))
34
- end
35
-
36
- private
37
-
38
- def current_rules(root)
39
- raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
40
- Array(raw["rules"])
41
- end
42
-
43
- def parse_candidate(yaml_text)
44
- raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
45
- return Value::Result.failure(:usage_error, "candidate is not a YAML mapping") unless raw.is_a?(Hash)
46
-
47
- Array(raw["rules"])
48
- rescue Psych::Exception => e
49
- Value::Result.failure(:usage_error, "candidate YAML parse error: #{e.message}")
50
- end
51
- end
52
- end
53
- end
54
- end