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
data/lib/textus/schema.rb DELETED
@@ -1,100 +0,0 @@
1
- require "yaml"
2
-
3
- module Textus
4
- class Schema
5
- attr_reader :name, :required, :optional, :fields, :raw
6
-
7
- def self.load(path)
8
- raw = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time], aliases: false)
9
- new(raw)
10
- end
11
-
12
- def initialize(raw)
13
- @raw = raw || {}
14
- @name = @raw["name"]
15
- @required = Array(@raw["required"])
16
- @optional = Array(@raw["optional"])
17
- @fields = @raw["fields"] || {}
18
- end
19
-
20
- def to_h
21
- @raw
22
- end
23
-
24
- def maintained_by(field)
25
- meta = @fields[field] or return nil
26
- meta["maintained_by"]
27
- end
28
-
29
- # Returns the list of field names whose spec is a Hash but lacks the
30
- # 'maintained_by' key. Used by Doctor::Check::UnownedSchemaFields.
31
- def unowned_fields
32
- @fields.each_with_object([]) do |(name, spec), acc|
33
- next unless spec.is_a?(Hash)
34
-
35
- acc << name if spec["maintained_by"].nil?
36
- end
37
- end
38
-
39
- def evolution
40
- raw = @raw["evolution"] || {}
41
- raw.each_with_object({}) do |(k, v), h|
42
- h[k] = v.is_a?(Date) || v.is_a?(Time) ? v.to_s : v
43
- end
44
- end
45
-
46
- # Returns nil on success; raises SchemaViolation on hard failure.
47
- # Unknown fields produce warnings, returned as a String[] alongside.
48
- def validate!(frontmatter)
49
- missing = @required - frontmatter.keys
50
- raise SchemaViolation.new("missing" => missing) unless missing.empty?
51
-
52
- known = (@required + @optional).uniq
53
- frontmatter.each do |k, v|
54
- next unless @fields.key?(k)
55
-
56
- check_type!(k, v, @fields[k])
57
- end
58
-
59
- warnings = frontmatter.keys - known
60
- warnings.map { |w| "unknown field: #{w}" }
61
- end
62
-
63
- private
64
-
65
- def check_type!(field, value, spec) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
- type = spec["type"]
67
- case type
68
- when "string"
69
- bad!(field, "expected string") unless value.is_a?(String)
70
- if (max = spec["max"]) && value.bytesize > max
71
- bad!(field, "exceeds max #{max}")
72
- end
73
- when "number"
74
- bad!(field, "expected number") unless value.is_a?(Numeric)
75
- when "boolean"
76
- bad!(field, "expected boolean") unless [true, false].include?(value)
77
- when "enum"
78
- values = Array(spec["values"])
79
- bad!(field, "not in enum #{values.inspect}") unless values.include?(value)
80
- when "array"
81
- bad!(field, "expected array") unless value.is_a?(Array)
82
- if (items = spec["items"])
83
- value.each_with_index { |v, i| check_type!("#{field}[#{i}]", v, items) }
84
- end
85
- when "object"
86
- bad!(field, "expected object") unless value.is_a?(Hash)
87
- if (sub = spec["fields"])
88
- sub.each { |fk, fspec| check_type!("#{field}.#{fk}", value[fk], fspec) if value.key?(fk) }
89
- end
90
- when nil
91
- # untyped — no check
92
- # unknown type spec — vendor extension; ignore
93
- end
94
- end
95
-
96
- def bad!(field, reason)
97
- raise SchemaViolation.new("field" => field, "reason" => reason)
98
- end
99
- end
100
- end
@@ -1,173 +0,0 @@
1
- module Textus
2
- class Store
3
- class Container
4
- Infrastructure = Data.define(:file_store, :schemas, :audit_log, :job_store, :layout)
5
- Coordination = Data.define(:manifest, :workflows, :pipeline)
6
-
7
- def self.attribute_names
8
- @attribute_names ||= [:root] + Infrastructure.members + Coordination.members
9
- end
10
-
11
- def initialize(infra, coord)
12
- @infra = infra
13
- @coord = coord
14
- end
15
-
16
- attr_reader :infra, :coord, :pipeline, :reader, :writer
17
-
18
- def root
19
- @infra.layout.root
20
- end
21
-
22
- Infrastructure.members.each do |name|
23
- define_method(name) { @infra.public_send(name) }
24
- end
25
-
26
- Coordination.members.each do |name|
27
- define_method(name) { @coord.public_send(name) }
28
- end
29
-
30
- def wire!(pipeline:, reader:, writer:)
31
- @pipeline = pipeline
32
- @reader = reader
33
- @writer = writer
34
- @coord = Coordination.new(
35
- manifest: @coord.manifest,
36
- workflows: @coord.workflows,
37
- pipeline: pipeline,
38
- )
39
- self
40
- end
41
-
42
- def self.build(infra, coord_seed)
43
- coord = Coordination.new(
44
- manifest: coord_seed.manifest,
45
- workflows: coord_seed.workflows,
46
- pipeline: nil,
47
- )
48
- container = new(infra, coord)
49
- pipeline = build_pipeline(container)
50
- reader = Textus::Store::Entry::Reader.from(container: container)
51
- writer = create_writer_factory(container)
52
- container.wire!(pipeline: pipeline, reader: reader, writer: writer)
53
- end
54
-
55
- def self.orchestration_for(container)
56
- Orchestration.new(
57
- list_keys: Handlers::Read::ListKeys.new(manifest: container.manifest, job_store: container.job_store),
58
- move_key: Handlers::Write::MoveKey.new(container: container, manifest: container.manifest),
59
- delete_key: Handlers::Write::DeleteKey.new(container: container),
60
- audit_entries: Handlers::Read::AuditEntries.new(manifest: container.manifest, audit_log: container.audit_log),
61
- )
62
- end
63
-
64
- def self.build_pipeline(container) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
65
- registry = Dispatch::HandlerRegistry.new
66
- fe = freshness_evaluator(container)
67
- orch = orchestration_for(container)
68
-
69
- registry.register(Dispatch::Contracts::GetEntry,
70
- Handlers::Read::GetEntry.new(container: container, freshness_evaluator: fe))
71
- registry.register(Dispatch::Contracts::PutEntry,
72
- Handlers::Write::PutEntry.new(container: container))
73
- registry.register(Dispatch::Contracts::ListKeys,
74
- Handlers::Read::ListKeys.new(manifest: container.manifest, job_store: container.job_store))
75
- registry.register(Dispatch::Contracts::DeleteKey,
76
- Handlers::Write::DeleteKey.new(container: container))
77
- registry.register(Dispatch::Contracts::MoveKey,
78
- Handlers::Write::MoveKey.new(container: container, manifest: container.manifest))
79
- registry.register(Dispatch::Contracts::ProposeEntry,
80
- Handlers::Write::ProposeEntry.new(container: container))
81
- registry.register(Dispatch::Contracts::AcceptProposal,
82
- Handlers::Write::AcceptProposal.new(container: container))
83
- registry.register(Dispatch::Contracts::RejectProposal,
84
- Handlers::Write::RejectProposal.new(container: container))
85
- registry.register(Dispatch::Contracts::EnqueueJob,
86
- Handlers::Write::EnqueueJob.new(job_store: container.job_store))
87
- registry.register(Dispatch::Contracts::WhereEntry,
88
- Handlers::Read::WhereEntry.new(manifest: container.manifest))
89
- registry.register(Dispatch::Contracts::UidEntry,
90
- Handlers::Read::UidEntry.new(container: container))
91
- registry.register(Dispatch::Contracts::DepsEntry,
92
- Handlers::Read::DepsEntry.new(manifest: container.manifest))
93
- registry.register(Dispatch::Contracts::RdepsEntry,
94
- Handlers::Read::RdepsEntry.new(manifest: container.manifest))
95
- registry.register(Dispatch::Contracts::BootStore,
96
- Handlers::Maintenance::BootStore.new(container: container))
97
- registry.register(Dispatch::Contracts::DoctorStore,
98
- Handlers::Maintenance::DoctorStore.new(container: container))
99
- registry.register(Dispatch::Contracts::PublishedEntries,
100
- Handlers::Maintenance::PublishedEntries.new(manifest: container.manifest))
101
- registry.register(Dispatch::Contracts::RuleExplain,
102
- Handlers::Maintenance::RuleExplain.new(manifest: container.manifest))
103
- registry.register(Dispatch::Contracts::RuleList,
104
- Handlers::Maintenance::RuleList.new(manifest: container.manifest))
105
- registry.register(Dispatch::Contracts::SchemaEnvelope,
106
- Handlers::Maintenance::SchemaEnvelope.new(manifest: container.manifest, schemas: container.schemas))
107
- registry.register(Dispatch::Contracts::DrainStore,
108
- Handlers::Maintenance::DrainStore.new(container: container, job_store: container.job_store))
109
- registry.register(Dispatch::Contracts::IngestEntry,
110
- Handlers::Maintenance::IngestEntry.new(container: container))
111
- registry.register(Dispatch::Contracts::JobsAction,
112
- Handlers::Maintenance::JobsAction.new(job_store: container.job_store))
113
- registry.register(Dispatch::Contracts::RuleLint,
114
- Handlers::Maintenance::RuleLint.new(manifest: container.manifest))
115
- registry.register(Dispatch::Contracts::DataMv,
116
- Handlers::Write::DataMv.new(container: container))
117
- registry.register(Dispatch::Contracts::AuditEntries,
118
- Handlers::Read::AuditEntries.new(manifest: container.manifest, audit_log: container.audit_log))
119
- registry.register(Dispatch::Contracts::PulseEntries,
120
- Handlers::Read::PulseEntries.new(
121
- manifest: container.manifest,
122
- audit_log: container.audit_log,
123
- file_store: container.file_store,
124
- job_store: container.job_store,
125
- orchestration: orch,
126
- ))
127
- registry.register(Dispatch::Contracts::BlameEntry,
128
- Handlers::Read::BlameEntry.new(manifest: container.manifest, orchestration: orch))
129
- registry.register(Dispatch::Contracts::KeyMvPrefix,
130
- Handlers::Write::KeyMvPrefix.new(orchestration: orch))
131
- registry.register(Dispatch::Contracts::KeyDeletePrefix,
132
- Handlers::Write::KeyDeletePrefix.new(orchestration: orch))
133
-
134
- Dispatch::Pipeline.new(
135
- registry: registry,
136
- container: container,
137
- middleware: [
138
- Dispatch::Middleware::Binder.new,
139
- Dispatch::Middleware::Auth.new,
140
- Dispatch::Middleware::AuditIndex.new(
141
- job_store: container.job_store,
142
- audit_log: container.audit_log,
143
- ),
144
- Dispatch::Middleware::Cascade.new,
145
- ],
146
- )
147
- end
148
- private_class_method :build_pipeline
149
-
150
- def self.create_writer_factory(container)
151
- lambda do |call|
152
- Textus::Store::Entry::Writer.new(
153
- file_store: container.file_store,
154
- manifest: container.manifest,
155
- schemas: container.schemas,
156
- audit_log: container.audit_log,
157
- call: call,
158
- reader: container.reader,
159
- layout: container.layout,
160
- )
161
- end
162
- end
163
-
164
- def self.freshness_evaluator(container)
165
- Store::Freshness::TtlEvaluator.new(
166
- manifest: container.manifest,
167
- file_stat: Textus::Port::Storage::FileStat.new,
168
- clock: Textus::Port::Clock.new,
169
- )
170
- end
171
- end
172
- end
173
- end
@@ -1,26 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Store
5
- # Per-role cursor cache under <root>/.state/cursors/<role>. A convenience so
6
- # `textus pulse` (no --since) means "since I last looked". Gitignored;
7
- # losing it just re-emits recent deltas, never corrupts the store. ADR 0036/0038.
8
- class Cursor
9
- def initialize(root:, role:)
10
- @path = Store::Layout.new(root).cursor_path(role)
11
- end
12
-
13
- def read
14
- Integer(File.read(@path).strip)
15
- rescue Errno::ENOENT, ArgumentError
16
- 0
17
- end
18
-
19
- def write(seq)
20
- FileUtils.mkdir_p(File.dirname(@path))
21
- File.write(@path, seq.to_s)
22
- seq
23
- end
24
- end
25
- end
26
- end
@@ -1,47 +0,0 @@
1
- module Textus
2
- class Store
3
- module Entry
4
- # Read-only counterpart to EnvelopeWriter. Resolves a key, reads the
5
- # bytes, parses them via the format strategy, and hands back an
6
- # Envelope. Used by Mv (pre-move inspection) and by EnvelopeWriter
7
- # (existing-meta lookup for the uid/sources preservation step in #put).
8
- #
9
- # No audit, no events, no permission checks — those live one layer up.
10
- class Reader
11
- def self.from(container:)
12
- # Prefer a cached reader on the container (injection point) for
13
- # tests and alternative runtimes. Fall back to constructing one.
14
- return container.reader if container.respond_to?(:reader) && container.reader
15
-
16
- new(file_store: container.file_store, manifest: container.manifest,
17
- layout: container.layout)
18
- end
19
-
20
- def initialize(file_store:, manifest:, layout:)
21
- @file_store = file_store
22
- @manifest = manifest
23
- @layout = layout
24
- end
25
-
26
- def read(key)
27
- res = @manifest.resolver.resolve(key)
28
- path = res.path
29
- return nil unless @file_store.exists?(path)
30
-
31
- mentry = res.entry
32
- raw = @file_store.read(path)
33
- parsed = Format.for(mentry.format).parse(raw, path: path)
34
- Textus::Value::Envelope.build(
35
- key: key, mentry: mentry, path: path,
36
- meta: parsed["_meta"], body: parsed["body"],
37
- etag: Value::Etag.for_bytes(raw), content: parsed["content"]
38
- )
39
- end
40
-
41
- def exists?(key)
42
- @file_store.exists?(@manifest.resolver.resolve(key).path)
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,219 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Store
5
- module Entry
6
- # Owns the write pipeline (validate, serialize, etag-check, write, audit).
7
- # Talks to ports (FileStore, Schemas, AuditLog, Manifest) and an
8
- # Reader for the existing-uid lookup.
9
- #
10
- # Invariant: every public method's final action is @audit_log.append(...).
11
- #
12
- # No permission check, no event firing — those belong to the caller
13
- # (Write::Put / ::Delete / ::Mv).
14
- class Writer
15
- def self.from(container:, call:)
16
- # If the container exposes a writer factory (in tests we set this),
17
- # use it. Otherwise, construct a fresh Writer.
18
- return container.writer.call(call) if container.respond_to?(:writer) && container.writer
19
-
20
- new(
21
- file_store: container.file_store, manifest: container.manifest,
22
- schemas: container.schemas, audit_log: container.audit_log,
23
- call: call, reader: Reader.from(container: container),
24
- layout: container.layout
25
- )
26
- end
27
-
28
- def initialize(file_store:, manifest:, schemas:, audit_log:, call:, reader:, layout:)
29
- @file_store = file_store
30
- @manifest = manifest
31
- @schemas = schemas
32
- @audit_log = audit_log
33
- @call = call
34
- @reader = reader
35
- @layout = layout
36
- end
37
-
38
- def put(key, mentry:, payload:, if_etag: nil)
39
- path = resolve_path(key)
40
- meta = payload.meta || {}
41
- content = payload.content
42
-
43
- existing_env = read_existing(key)
44
- existing_meta = existing_env ? existing_env.meta : {}
45
- meta, content = inject_meta(meta, content, existing_meta, mentry.format)
46
-
47
- bytes, eff_meta, eff_body, eff_content = serialize_entry(mentry, path, meta, payload, content)
48
-
49
- enforce_name_match!(path, eff_meta, mentry.format)
50
- validate_schema(mentry, eff_meta, eff_content)
51
- validate_raw(eff_meta, eff_content, mentry.lane, mentry.format)
52
-
53
- etag_before = check_etag!(path, key, if_etag)
54
- write_bytes(path, bytes)
55
-
56
- envelope = build_envelope(key, mentry, path, eff_meta, eff_body, eff_content, bytes)
57
- audit_put(key, etag_before, envelope.etag)
58
- envelope
59
- end
60
-
61
- def delete(key, mentry: nil, if_etag: nil) # rubocop:disable Lint/UnusedMethodArgument
62
- # `mentry:` is accepted for symmetry with `put` / `move` and to
63
- # leave room for future format-specific delete hooks; no field
64
- # on it is needed today.
65
- path = @manifest.resolver.resolve(key).path
66
- raise UnknownKey.new(key, suggestions: @manifest.resolver.suggestions_for(key)) unless @file_store.exists?(path)
67
-
68
- etag_before = @file_store.etag(path)
69
- raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
70
-
71
- @file_store.delete(path)
72
- prune_empty_parents(path)
73
- @audit_log.append(
74
- role: @call.role, verb: "key_delete", key: key,
75
- etag_before: etag_before, etag_after: nil,
76
- extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
77
- )
78
- end
79
-
80
- def move(from_key:, to_key:, new_mentry:, if_etag: nil)
81
- from_path = @manifest.resolver.resolve(from_key).path
82
- to_path = @manifest.resolver.resolve(to_key).path
83
- raise UnknownKey.new(from_key, suggestions: @manifest.resolver.suggestions_for(from_key)) unless @file_store.exists?(from_path)
84
-
85
- etag_before = @file_store.etag(from_path)
86
- raise EtagMismatch.new(from_key, if_etag, etag_before) if if_etag && if_etag != etag_before
87
-
88
- @file_store.mv(from_path, to_path)
89
- prune_empty_parents(from_path)
90
- basename = to_key.split(".").last
91
- Format.for(new_mentry.format).rewrite_name(to_path, basename)
92
- etag_after = Value::Etag.for_file(to_path)
93
-
94
- envelope = @reader.read(to_key)
95
-
96
- extras = {
97
- "from_key" => from_key, "to_key" => to_key,
98
- "from_path" => from_path, "to_path" => to_path,
99
- "uid" => envelope.uid
100
- }
101
- extras["correlation_id"] = @call.correlation_id if @call.correlation_id
102
-
103
- @audit_log.append(
104
- role: @call.role, verb: "key_mv", key: to_key,
105
- etag_before: etag_before, etag_after: etag_after,
106
- extras: extras
107
- )
108
-
109
- envelope
110
- end
111
-
112
- private
113
-
114
- # After a file leaves a directory (delete or move-source), remove any
115
- # now-empty parent dirs so bulk move/delete doesn't accrue orphan dirs
116
- # (F3 of #161). Floored at the entry's *zone directory* — a zone is a
117
- # declared, first-class container, so its own dir is preserved even when
118
- # momentarily empty; only the sub-dirs the bulk op carved out are
119
- # pruned. Stops at the first non-empty ancestor, so a dir holding a
120
- # `.gitkeep` or sibling entries survives. Best-effort: a lost race or a
121
- # non-empty dir is silently fine, never fatal to the write.
122
- def prune_empty_parents(path)
123
- floor = @layout.lane_floor(path)
124
- return unless floor
125
-
126
- dir = File.dirname(path)
127
- while dir.start_with?("#{floor}/") && @file_store.dir_empty?(dir)
128
- @file_store.rmdir(dir)
129
- dir = File.dirname(dir)
130
- end
131
- rescue SystemCallError
132
- nil
133
- end
134
-
135
- def read_existing(key)
136
- @reader.read(key)
137
- end
138
-
139
- def inject_meta(meta, content, existing_meta, format)
140
- Envelope::Meta.inject_all(
141
- meta, content, existing_meta,
142
- format: format,
143
- etag_for: method(:resolve_source_etag)
144
- )
145
- end
146
-
147
- def resolve_source_etag(key)
148
- path = @manifest.resolver.resolve(key).path
149
- return nil unless @file_store.exists?(path)
150
-
151
- Value::Etag.for_file(path)
152
- rescue Textus::Error
153
- nil
154
- end
155
-
156
- def resolve_path(key)
157
- @manifest.resolver.resolve(key).path
158
- end
159
-
160
- def serialize_entry(mentry, path, meta, payload, content)
161
- Textus::Format.for(mentry.format).serialize_for_put(
162
- meta: meta, body: payload.body, content: content, path: path,
163
- )
164
- end
165
-
166
- def enforce_name_match!(path, meta, format)
167
- Textus::Format.for(format).enforce_name_match!(path, meta)
168
- end
169
-
170
- def validate_schema(mentry, eff_meta, eff_content)
171
- schema = @schemas.fetch_or_nil(mentry.schema)
172
- return unless schema
173
-
174
- Format.for(mentry.format).validate_against(
175
- schema,
176
- { "_meta" => eff_meta, "content" => eff_content },
177
- )
178
- end
179
-
180
- def validate_raw(eff_meta, eff_content, lane, format)
181
- Textus::Format.for(format).validate_raw_entry!(
182
- { "_meta" => eff_meta, "content" => eff_content },
183
- lane,
184
- )
185
- end
186
-
187
- def check_etag!(path, key, if_etag)
188
- etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
189
- raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
190
-
191
- etag_before
192
- end
193
-
194
- def write_bytes(path, bytes)
195
- @file_store.write(path, bytes)
196
- end
197
-
198
- def build_envelope(key, mentry, path, eff_meta, eff_body, eff_content, bytes = nil)
199
- raw = bytes || @file_store.read(path)
200
- Textus::Value::Envelope.build(
201
- key: key, mentry: mentry, path: path,
202
- meta: eff_meta, body: eff_body,
203
- etag: Value::Etag.for_bytes(raw),
204
- content: eff_content
205
- )
206
- end
207
-
208
- def audit_put(key, etag_before, etag_after)
209
- extras = @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
210
- @audit_log.append(
211
- role: @call.role, verb: "put", key: key,
212
- etag_before: etag_before, etag_after: etag_after,
213
- extras: extras
214
- )
215
- end
216
- end
217
- end
218
- end
219
- end
@@ -1,61 +0,0 @@
1
- require "securerandom"
2
-
3
- module Textus
4
- class Store
5
- module Envelope
6
- module Meta
7
- NO_META_FORMATS = %w[text].freeze
8
-
9
- FIELDS = {
10
- "uid" => {
11
- inject: lambda { |meta, content, existing_meta, **_opts|
12
- m = meta.is_a?(Hash) ? meta.dup : {}
13
- existing = existing_meta.is_a?(Hash) ? existing_meta["uid"] : nil
14
- m["uid"] = existing || Textus::Value::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
15
- [m, content]
16
- },
17
- },
18
- "sources" => {
19
- inject: lambda { |meta, content, existing_meta, etag_for: nil|
20
- m = meta.is_a?(Hash) ? meta.dup : {}
21
- existing = existing_meta.is_a?(Hash) ? existing_meta["sources"] : nil
22
-
23
- if m.key?("sources")
24
- raise Textus::BadContent.new(nil, "_meta.sources must be an array") unless m["sources"].is_a?(Array)
25
-
26
- m["sources"] = m["sources"].map { |s| Meta.normalize_source!(s, etag_for) }
27
- elsif existing.is_a?(Array) && !existing.empty?
28
- m["sources"] = existing
29
- end
30
-
31
- [m, content]
32
- },
33
- },
34
- }.freeze
35
-
36
- def self.inject_all(meta, content, existing_meta = {}, format: nil, etag_for: nil)
37
- return [meta, content] if NO_META_FORMATS.include?(format)
38
-
39
- FIELDS.each_value do |field|
40
- meta, content = field[:inject].call(meta, content, existing_meta, etag_for: etag_for)
41
- end
42
-
43
- [meta, content]
44
- end
45
-
46
- def self.normalize_source!(src, etag_for)
47
- key = case src
48
- when String then src
49
- when Hash then src["key"]
50
- end
51
-
52
- raise Textus::BadContent.new(nil, "each source must be a string key or { key: } object") unless key.is_a?(String)
53
- raise Textus::BadContent.new(nil, "each source key must be a non-empty string") if key.empty?
54
-
55
- etag = etag_for&.call(key)
56
- etag ? { "key" => key, "etag" => etag } : { "key" => key }
57
- end
58
- end
59
- end
60
- end
61
- end