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,21 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Validators
5
- module FormatMatrix
6
- def self.call(entry, policy:) # rubocop:disable Lint/UnusedMethodArgument
7
- begin
8
- Textus::Format.for(entry.format).validate_path_extension(entry.path, entry.nested?)
9
- rescue UsageError => e
10
- raise UsageError.new("entry '#{entry.key}': #{e.message}")
11
- end
12
-
13
- return unless entry.format == "text" && !entry.schema.nil?
14
-
15
- raise UsageError.new("entry '#{entry.key}': text format must not declare a schema")
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,28 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Validators
5
- # Validates the per-entry `ignore:` field (ADR 0042): a list of
6
- # non-empty glob strings, allowed only on nested entries.
7
- module Ignore
8
- def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
9
- patterns = entry.raw["ignore"]
10
- return if patterns.nil?
11
-
12
- raise UsageError.new("entry '#{entry.key}': ignore requires nested: true") unless entry.nested?
13
-
14
- raise UsageError.new("entry '#{entry.key}': ignore must be a list of glob strings") unless patterns.is_a?(Array)
15
-
16
- patterns.each do |pat|
17
- next if pat.is_a?(String) && !pat.empty?
18
-
19
- raise UsageError.new(
20
- "entry '#{entry.key}': each ignore pattern must be a non-empty string (got #{pat.inspect})",
21
- )
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Validators
5
- # ADR 0049: one publish validator. Exclusivity among the publish keys is
6
- # enforced structurally by Publish.resolve (reached via #publish_mode),
7
- # and each mode's shape rules run *because that mode resolved* — replacing
8
- # the scattered pairwise "not-both" guards of the old PublishEach +
9
- # PublishTree validators. Misuse on a non-nested entry is still caught
10
- # here from raw, since the typed attrs stub nil on Base. (publish_each was
11
- # removed in 0.42.0 — ADR 0051; the publish_to/publish_tree keys were folded
12
- # into the `publish:` block in 0.43.0 — ADR 0052; Schema rejects the retired
13
- # flat keys at load.)
14
- module Publish
15
- def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
16
- unless entry.nested?
17
- # ADR 0094: publish: is now a list; use publish_tree (derived reader)
18
- # rather than raw.dig("publish", "tree") which breaks on an Array.
19
- raise UsageError.new("entry '#{entry.key}': publish.tree requires nested: true") if entry.publish_tree
20
-
21
- return
22
- end
23
-
24
- entry.publish_mode.validate!
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Validators
5
- REGISTERED = [
6
- Publish,
7
- Ignore,
8
- FormatMatrix,
9
- ].freeze
10
-
11
- def self.run_all(entry, policy:)
12
- REGISTERED.each { |v| v.call(entry, policy: policy) }
13
- nil
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,8 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- # Populated by each Entry::* subclass at load time.
5
- REGISTRY = {}
6
- end
7
- end
8
- end
@@ -1,51 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Matcher
5
- module_function
6
-
7
- def matches?(glob, key)
8
- glob_segs = glob.split(".")
9
- key_segs = key.split(".")
10
- consume(glob_segs, key_segs)
11
- end
12
-
13
- def specificity(glob)
14
- glob.split(".").reduce(0) do |s, seg|
15
- s + case seg
16
- when "**" then 0
17
- when "*" then 1
18
- else 10
19
- end
20
- end
21
- end
22
-
23
- def pick_most_specific(globs, key:)
24
- matching = globs.select { |g| matches?(g, key) }
25
- return nil if matching.empty?
26
-
27
- matching.max_by { |g| [specificity(g), -g.length, g] }
28
- end
29
-
30
- def self.consume(glob_segs, key_segs)
31
- return key_segs.empty? if glob_segs.empty?
32
-
33
- head = glob_segs.first
34
- rest = glob_segs[1..]
35
-
36
- if head == "**"
37
- return true if rest.empty?
38
-
39
- (0..key_segs.length).any? { |i| consume(rest, key_segs[i..]) }
40
- elsif key_segs.empty?
41
- false
42
- elsif head == "*" || head == key_segs.first
43
- consume(rest, key_segs[1..])
44
- else
45
- false
46
- end
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,22 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class AuthorHeld
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- holders = manifest.policy.roles_with_capability("author")
8
- pass = holders.include?(actor.to_s)
9
- reason = if pass
10
- nil
11
- elsif holders.empty?
12
- "no role holds the 'author' capability; #{action} is disabled"
13
- else
14
- "role '#{actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
15
- end
16
- { pass:, reason: }
17
- end
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,18 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class EtagMatch
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- if_etag = extra[:if_etag]
8
- return { pass: true } if if_etag.nil?
9
-
10
- current = envelope&.etag
11
- pass = current.nil? || current == if_etag
12
- { pass:, error: pass ? nil : Textus::EtagMismatch.new(key, if_etag, current) }
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,13 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class FreshWithin
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- { pass: true }
8
- end
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,31 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class LaneDeletableBy
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } if key.nil?
8
-
9
- mentry = manifest.resolver.resolve(key).entry
10
- is_raw = manifest.policy.declared_kind(mentry.lane.to_s) == :raw
11
- lane_verb = manifest.policy.verb_for_lane(mentry.lane.to_s)
12
- caps = Set.new(manifest.data.role_caps.fetch(actor.to_s, []))
13
-
14
- pass = if is_raw
15
- caps.include?("author")
16
- else
17
- caps.include?(lane_verb.to_s) || caps.include?("author")
18
- end
19
- return { pass: true } if pass
20
-
21
- extra_holders = is_raw ? ["author"] : [lane_verb.to_s, "author"]
22
- holders = extra_holders.flat_map { |v| manifest.policy.roles_with_capability(v) }.uniq
23
- { pass: false, error: Textus::WriteForbidden.new(mentry.key, mentry.lane, verb: lane_verb, holders:) }
24
- rescue Textus::UnknownKey
25
- { pass: true }
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,23 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class LaneWritableBy
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } if key.nil?
8
-
9
- mentry = manifest.resolver.resolve(key).entry
10
- lane_verb = manifest.policy.verb_for_lane(mentry.lane.to_s)
11
- caps = Set.new(manifest.data.role_caps.fetch(actor.to_s, []))
12
- return { pass: true } if caps.include?(lane_verb.to_s)
13
-
14
- holders = manifest.policy.roles_with_capability(lane_verb.to_s)
15
- { pass: false, error: Textus::WriteForbidden.new(mentry.key, mentry.lane, verb: lane_verb, holders:) }
16
- rescue Textus::UnknownKey
17
- { pass: true }
18
- end
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,25 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class RawLaneIngestOnly
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } if key.nil?
8
-
9
- mentry = manifest.resolver.resolve(key).entry
10
- return { pass: true } unless manifest.policy.declared_kind(mentry.lane.to_s) == :raw
11
- return { pass: true } if action == :ingest
12
-
13
- { pass: false, error: Textus::Error.new(
14
- :raw_lane_ingest_only,
15
- "raw lane '#{mentry.lane}' only accepts `textus ingest` — " \
16
- "use that verb instead of '#{action}'",
17
- ) }
18
- rescue Textus::UnknownKey
19
- { pass: true }
20
- end
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,24 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class RawWriteOnce
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } if key.nil?
8
-
9
- path = manifest.resolver.resolve(key).path
10
- return { pass: true } unless File.exist?(path)
11
-
12
- { pass: false, error: Textus::Error.new(
13
- :raw_write_once,
14
- "raw entry '#{key}' already exists; " \
15
- "delete it first (`textus key-delete #{key}`), then re-ingest",
16
- ) }
17
- rescue Textus::UnknownKey
18
- { pass: true }
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,41 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class SchemaValid
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } unless envelope
8
- return { pass: true } if key.nil?
9
-
10
- mentry = manifest.resolver.resolve(key).entry
11
- schema_ref = mentry.schema
12
- return { pass: true } unless schema_ref
13
- return { pass: true } unless schemas
14
-
15
- schema = schemas.fetch_or_nil(schema_ref)
16
- return { pass: true } unless schema
17
-
18
- frontmatter = envelope.meta&.dig("_meta") || envelope.meta || {}
19
- begin
20
- schema.validate!(frontmatter)
21
- { pass: true }
22
- rescue Textus::SchemaViolation => e
23
- { pass: false, reason: schema_reason(e) }
24
- end
25
- rescue Textus::UnknownKey
26
- { pass: true }
27
- end
28
-
29
- def self.schema_reason(err)
30
- d = err.details
31
- return err.message.dup unless d.is_a?(Hash)
32
- return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
33
- return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
34
-
35
- err.message.dup
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,20 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- class TargetIsCanon
6
- def self.call(manifest:, actor:, action:, key:, schemas: nil, envelope: nil, extra: {})
7
- return { pass: true } if key.nil?
8
-
9
- mentry = manifest.resolver.resolve(key).entry
10
- kind = manifest.policy.declared_kind(mentry.lane.to_s)
11
- pass = kind == :canon
12
- { pass:, reason: pass ? nil : "target lane '#{mentry.lane}' is not canon (kind: #{kind})" }
13
- rescue Textus::UnknownKey
14
- { pass: true }
15
- end
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,54 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- module Predicates
5
- FLOOR = {
6
- put: %w[lane_writable_by raw_lane_ingest_only],
7
- key_delete: %w[lane_deletable_by],
8
- key_mv: %w[lane_writable_by raw_lane_ingest_only],
9
- accept: %w[author_held],
10
- reject: %w[author_held],
11
- propose: %w[lane_writable_by raw_lane_ingest_only],
12
- key_mv_prefix: %w[lane_writable_by raw_lane_ingest_only],
13
- key_delete_prefix: %w[lane_writable_by raw_lane_ingest_only],
14
- ingest: %w[lane_writable_by raw_write_once],
15
- }.freeze
16
-
17
- CLASSES = {
18
- "lane_writable_by" => "LaneWritableBy",
19
- "author_held" => "AuthorHeld",
20
- "target_is_canon" => "TargetIsCanon",
21
- "etag_match" => "EtagMatch",
22
- "schema_valid" => "SchemaValid",
23
- "fresh_within" => "FreshWithin",
24
- "raw_lane_ingest_only" => "RawLaneIngestOnly",
25
- "raw_write_once" => "RawWriteOnce",
26
- "lane_deletable_by" => "LaneDeletableBy",
27
- }.freeze
28
-
29
- module_function
30
-
31
- def by_name(name)
32
- short = CLASSES.fetch(name.to_s) do
33
- raise Textus::UsageError.new("unknown predicate '#{name}'")
34
- end
35
- const_get(short)
36
- end
37
-
38
- def evaluate(manifest:, action:, actor:, key:, schemas: nil, envelope: nil, extra: {}, rule_predicates: [])
39
- failures = []
40
- (FLOOR.fetch(action, []) + rule_predicates).uniq.each do |pred_name|
41
- result = by_name(pred_name).call(
42
- manifest:, schemas:, actor:, action:, key:, envelope:, extra:,
43
- )
44
- next if result[:pass]
45
- raise result[:error] if result[:error]
46
-
47
- failures << [pred_name, result[:reason]]
48
- end
49
- raise Textus::GuardFailed.new(failures) unless failures.empty?
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,34 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- # One publish destination (ADR 0094). Exactly one of:
5
- # to-target { to:, template:?, inject_boot:? } — render data through a
6
- # template, or copy verbatim when no template
7
- # tree-target { tree: } — ADR 0052 subtree mirror
8
- # Provenance is NOT a publish flag — it lives in the data's `_meta`.
9
- class PublishTarget
10
- attr_reader :to, :tree, :template, :inject_boot
11
-
12
- def initialize(raw)
13
- if raw.key?("provenance")
14
- raise Textus::BadManifest.new("publish `provenance:` was removed (ADR 0094): provenance lives in the data's `_meta`")
15
- end
16
-
17
- @to = raw["to"]
18
- @tree = raw["tree"]
19
- raise Textus::BadManifest.new("a publish target needs exactly one of `to:` or `tree:`") unless @to.nil? ^ @tree.nil?
20
-
21
- @template = raw["template"]
22
- @inject_boot = raw["inject_boot"] == true
23
- return unless tree_target? && (@template || @inject_boot)
24
-
25
- raise Textus::BadManifest.new("a tree target takes no template/inject_boot (ADR 0094)")
26
- end
27
-
28
- def to_target? = !@to.nil?
29
- def tree_target? = !@tree.nil?
30
- def renders? = to_target? && !@template.nil?
31
- end
32
- end
33
- end
34
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- class React
5
- ALLOWED_KEYS = %w[on when do scope budget idempotency observe priority].freeze
6
-
7
- attr_reader :raw
8
-
9
- def initialize(raw:)
10
- raise Textus::BadManifest.new("react: must be a map") unless raw.is_a?(Hash)
11
-
12
- raw = raw.each_with_object({}) do |(key, value), out|
13
- normalized = key == true ? "on" : key.to_s
14
- out[normalized] = value
15
- end
16
- raise Textus::BadManifest.new("react.ttl is invalid; ttl belongs only to source.ttl or retention.ttl") if raw.key?("ttl")
17
-
18
- unknown = raw.keys - ALLOWED_KEYS
19
- raise Textus::BadManifest.new("react: unknown key(s): #{unknown.join(", ")}") unless unknown.empty?
20
-
21
- @raw = raw
22
- end
23
-
24
- def to_h
25
- @raw
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,29 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- # Garbage collection (ADR 0093). A glob-matched rule slot: when an entry
5
- # ages past `ttl`, retire it. Destructive only — runs on the full
6
- # `converge` pass, never on a write (ADR 0079's invariant). Orthogonal to
7
- # production (`source:`): an intake entry can re-pull hourly AND archive
8
- # after 90 days. `warn`/`refresh` are gone (refresh is implied by an
9
- # intake source; warn never fired after ADR 0089's pure-read get).
10
- class Retention
11
- ACTIONS = %i[drop archive].freeze
12
-
13
- attr_reader :action
14
-
15
- def initialize(raw)
16
- @ttl = raw["ttl"] or
17
- raise Textus::BadManifest.new("retention requires a 'ttl:'")
18
- @action = (raw["action"] || "").to_s.to_sym
19
- return if ACTIONS.include?(@action)
20
-
21
- raise Textus::BadManifest.new("retention action must be one of #{ACTIONS.join("|")}, got #{raw["action"].inspect}")
22
- end
23
-
24
- def ttl_seconds = Textus::Value::Duration.seconds(@ttl)
25
- def destructive? = true
26
- end
27
- end
28
- end
29
- end
@@ -1,30 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Policy
4
- # An entry's external-generator declaration. `source:` now means ONLY
5
- # `from: external` — out-of-band runner; textus never invokes it, only
6
- # detects drift (doctor generator_drift check).
7
- # `from: fetch` and `from: derive` are removed; those concerns now live
8
- # in workflow files under .textus/workflows/.
9
- class Source
10
- attr_reader :command, :sources
11
-
12
- def initialize(raw)
13
- from = raw["from"].to_s
14
- unless from == "external"
15
- raise Textus::BadManifest.new(
16
- "from: #{from} is removed — use a workflow file under .textus/workflows/ instead",
17
- )
18
- end
19
-
20
- @command = raw["command"] or
21
- raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
22
- @sources = raw["sources"] || []
23
- end
24
-
25
- def external? = true
26
- def kind = :external
27
- end
28
- end
29
- end
30
- end
@@ -1,95 +0,0 @@
1
- module Textus
2
- class Manifest
3
- # Authority over lanes and roles derived from a Manifest::Data snapshot.
4
- # Encapsulates the lookups previously living on Manifest itself
5
- # (lane_writers, permission_for). Write authority is derived from
6
- # capabilities x lane-kind (ADR 0030): each lane-kind requires one verb
7
- # (Schema::KIND_REQUIRES_VERB) and a role may write a lane iff its caps
8
- # include that verb (verb_for_lane, roles_with_capability). Derived /
9
- # proposal-queue status is authoritative via the declared-kind family
10
- # (declared_kind, derived_entry?, queue_lane?, queue_lane).
11
- class Policy
12
- def initialize(data)
13
- @data = data
14
- end
15
-
16
- # The capability a lane's kind requires to be written, or nil if the
17
- # lane declares no kind. declared_kind returns a Symbol; the table is
18
- # keyed by String.
19
- def verb_for_lane(lane_name)
20
- kind = declared_kind(lane_name)
21
- kind && Schema::KIND_REQUIRES_VERB[kind.to_s]
22
- end
23
-
24
- # Names of roles whose declared caps include `verb`.
25
- def roles_with_capability(verb)
26
- @data.role_caps.select { |_name, caps| caps.include?(verb) }.keys
27
- end
28
-
29
- # The conventional automated proposer: a role that can propose but is not
30
- # the author-anchor (so it resolves to `agent`, not `human`, under the
31
- # default mapping). Falls back to the first proposer, then nil.
32
- def proposer_role
33
- proposers = roles_with_capability("propose")
34
- (proposers - roles_with_capability("author")).first || proposers.first
35
- end
36
-
37
- # The role textus acts AS for a system-initiated operation requiring
38
- # `verb` (no human passed --as). Capability-derived — a role name that
39
- # exists in the manifest, or nil. Never a hardcoded literal (ADR 0044).
40
- def actor_for(verb)
41
- roles_with_capability(verb).first
42
- end
43
-
44
- # The kind declared on a lane in the manifest, or nil if undeclared.
45
- def declared_kind(lane_name)
46
- @data.declared_lane_kinds[lane_name]
47
- end
48
-
49
- # Lane names declaring `kind` (a Symbol), in manifest order. Lets callers
50
- # (boot) name a kind's live lane instance(s) instead of hardcoding names.
51
- def lanes_of_kind(kind)
52
- @data.declared_lane_kinds.select { |_name, k| k == kind }.keys
53
- end
54
-
55
- # The single lane declaring `kind: queue`, or nil. Schema guarantees <=1.
56
- def queue_lane
57
- @data.declared_lane_kinds.key(:queue)
58
- end
59
-
60
- # ADR 0091: derived-ness is a property of the ENTRY, not its lane (one
61
- # machine lane holds both intake and derived entries). Resolve the entry
62
- # and ask it directly. Returns false if entries are not yet built
63
- # (validator phase during Data#initialize) — validators must not rely on
64
- # cross-entry state during construction.
65
- def derived_entry?(_key)
66
- false
67
- end
68
-
69
- # The single lane declaring kind: machine, or nil.
70
- def machine_lane
71
- @data.declared_lane_kinds.key(:machine)
72
- end
73
-
74
- # A lane is a proposal queue iff it declares kind: queue.
75
- def queue_lane?(lane_name)
76
- declared_kind(lane_name) == :queue
77
- end
78
-
79
- # The lane a proposer role writes proposals into: the single lane that
80
- # declares kind: queue, when the role can write it. Returns nil if there
81
- # is no queue lane or the role cannot write it.
82
- def propose_lane_for(role)
83
- return nil if role.nil?
84
-
85
- q = queue_lane
86
- return nil unless q
87
-
88
- q_verb = verb_for_lane(q)
89
- return nil unless roles_with_capability(q_verb).include?(role)
90
-
91
- q
92
- end
93
- end
94
- end
95
- end