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,105 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- class Base < Entry
5
- attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
6
-
7
- # rubocop:disable Lint/MissingSuper
8
- def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
9
- @raw = raw
10
- @key = key
11
- @path = path
12
- @lane = lane
13
- @schema = schema
14
- @owner = owner
15
- @format = format
16
- @publish_targets = Array(publish_targets)
17
- end
18
- # rubocop:enable Lint/MissingSuper
19
-
20
- def lane_writers(policy)
21
- verb = policy.verb_for_lane(@lane)
22
- policy.roles_with_capability(verb)
23
- rescue UsageError => e
24
- raise UsageError.new("entry '#{@key}': #{e.message}")
25
- end
26
-
27
- def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
28
-
29
- def nested? = false
30
- def derived? = false
31
- def intake? = false
32
- def leaf? = false
33
-
34
- # Production traits. Default false on Base (a leaf/intake entry is neither
35
- # an out-of-band command nor a projection); Produced overrides both from
36
- # its source. Lets publish modes call these without a `respond_to?` guard.
37
- def external? = false
38
- def projection? = false
39
-
40
- alias zone lane
41
- alias in_proposal_zone? in_proposal_lane?
42
-
43
- # Whether git should track this entry's file. Default true; an entry
44
- # marked `tracked: false` in the manifest stays protocol-readable but is
45
- # listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
46
- # reads from raw here rather than threading through every constructor.
47
- def tracked? = @raw["tracked"] != false
48
-
49
- # Single source of truth is @publish_targets (ADR 0094). These
50
- # derive the ADR-0049/0052 views the publish modes consume.
51
- def publish_to = @publish_targets.select(&:to_target?).map(&:to)
52
- def publish_tree = @publish_targets.find(&:tree_target?)&.tree
53
-
54
- # Nil stubs for cross-cutting optional attrs. Subclasses override the
55
- # ones they own. Validators and serializers can call these directly
56
- # without `respond_to?` guards.
57
- def events = {}
58
- def ignore = []
59
-
60
- # Per-entry ignore (ADR 0042). Base entries enumerate no tree, so
61
- # nothing is ever ignored; Nested overrides with real patterns.
62
- def ignored?(_rel_path) = false
63
-
64
- # Minimal context object passed into entry `publish_via` hooks.
65
- # Everything beyond the three primitives is derived.
66
- PublishContext = ::Data.define(:container, :call, :reader) do
67
- def manifest = container.manifest
68
- def root = container.root
69
- def repo_root = File.dirname(container.root)
70
-
71
- # No-op: event bus removed in workflow redesign; callers that fire
72
- # :entry_published / :entry_produced remain unchanged in the source.
73
- def emit(_event, **_payload) = nil
74
-
75
- # Read a named template from the store's templates/ directory.
76
- # Raises TemplateError when the file doesn't exist.
77
- def read_template(name)
78
- path = container.layout.template_path(name)
79
- unless File.exist?(path)
80
- raise Textus::TemplateError.new(
81
- "template '#{name}' not found",
82
- template_name: name,
83
- )
84
- end
85
- File.read(path)
86
- end
87
- end
88
-
89
- # ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
90
- # publish algorithm. A plain entry publishes via ToPaths (publish_to) or
91
- # None; Nested resolves among the key/path-driven modes. Derived
92
- # overrides publish_via to materialize first.
93
- def publish_mode
94
- @publish_mode ||= Publish.resolve(self)
95
- end
96
-
97
- # Returns: { kind: :built|:leaves, value: ... } to be accumulated by
98
- # Write::Build, or nil to skip.
99
- def publish_via(pctx, prefix: nil)
100
- publish_mode.publish(pctx, prefix: prefix)
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,46 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- # Pure glob matcher backing per-entry `ignore:` patterns (ADR 0042).
5
- # `rel_path` is the slash-joined path of a candidate file relative to the
6
- # entry's base directory.
7
- #
8
- # Matching is segment-wise so the `**` globstar means "zero or more path
9
- # segments" — `File.fnmatch` alone cannot express this (under
10
- # FNM_PATHNAME a trailing `**` will not cross a `/`; without it a leading
11
- # `**/` will not match zero leading segments). So `**/node_modules/**`
12
- # catches the `node_modules` subtree at any depth, including the store
13
- # root, and the directory entry itself.
14
- #
15
- # Within a single segment, matching delegates to `File.fnmatch` with
16
- # FNM_EXTGLOB, so a single `*` is anchored to that segment (it does not
17
- # cross `/`) and `{a,b}` alternation works.
18
- module IgnoreMatcher
19
- SEGMENT_FLAGS = File::FNM_EXTGLOB
20
-
21
- def self.match?(patterns, rel_path)
22
- path_segs = rel_path.split("/").reject(&:empty?)
23
- Array(patterns).any? do |pat|
24
- match_segments(pat.split("/").reject(&:empty?), path_segs)
25
- end
26
- end
27
-
28
- # Classic globstar matcher. `**` matches zero or more whole segments;
29
- # any other pattern segment matches exactly one path segment via fnmatch.
30
- def self.match_segments(pat_segs, path_segs)
31
- return path_segs.empty? if pat_segs.empty?
32
-
33
- if pat_segs.first == "**"
34
- match_segments(pat_segs[1..], path_segs) ||
35
- (!path_segs.empty? && match_segments(pat_segs, path_segs[1..]))
36
- else
37
- !path_segs.empty? &&
38
- File.fnmatch?(pat_segs.first, path_segs.first, SEGMENT_FLAGS) &&
39
- match_segments(pat_segs[1..], path_segs[1..])
40
- end
41
- end
42
- private_class_method :match_segments
43
- end
44
- end
45
- end
46
- end
@@ -1,17 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- class Leaf < Base
5
- KIND = :leaf
6
-
7
- def leaf? = true
8
-
9
- def self.from_raw(common, _raw)
10
- new(**common)
11
- end
12
-
13
- Entry::REGISTRY[KIND] = self
14
- end
15
- end
16
- end
17
- end
@@ -1,37 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- # A directory entry: enumerates a tree of leaves and resolves to one
5
- # publish mode (ADR 0049). The publish algorithms themselves live in
6
- # Entry::Publish::* — Nested is just the value (attributes + ignore
7
- # predicate) those modes read.
8
- class Nested < Base
9
- attr_reader :ignore
10
-
11
- def initialize(ignore: nil, **rest)
12
- super(**rest)
13
- @ignore = Array(ignore)
14
- end
15
-
16
- def nested? = true
17
-
18
- # True when `rel_path` (slash-joined, relative to the entry base) matches
19
- # any configured ignore glob. Evaluated ABOVE key-legality (ADR 0042):
20
- # an ignored path is excluded, never judged.
21
- def ignored?(rel_path) = IgnoreMatcher.match?(@ignore, rel_path)
22
-
23
- KIND = :nested
24
-
25
- def self.from_raw(common, raw)
26
- # publish_tree is derived from publish_targets (ADR 0094) via Base#publish_tree
27
- new(
28
- ignore: raw["ignore"],
29
- **common,
30
- )
31
- end
32
-
33
- Entry::REGISTRY[KIND] = self
34
- end
35
- end
36
- end
37
- end
@@ -1,99 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Parser
5
- def self.call(raw)
6
- key = raw["key"] or raise UsageError.new("manifest entry missing key")
7
- lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
8
-
9
- raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
10
- kind = raw_kind.to_sym
11
- if %i[derived intake].include?(kind)
12
- raise BadManifest.new(
13
- "entry '#{key}': kind: #{kind} was collapsed into `kind: produced` (ADR 0095) — " \
14
- "the produce method is `source.from` (#{kind == :intake ? "handler" : "project|command"})",
15
- )
16
- end
17
-
18
- explicit_path = raw["path"]
19
- format = resolve_format(raw, explicit_path)
20
- path = explicit_path || derive_path(key, kind, format)
21
-
22
- common = {
23
- raw: raw,
24
- key: key, path: path, lane: lane,
25
- schema: raw["schema"], owner: raw["owner"],
26
- format: format,
27
- publish_targets: publish_targets(raw)
28
- }
29
-
30
- klass = Entry::REGISTRY[kind] or
31
- raise BadManifest.new("entry '#{key}': unknown kind: #{kind.inspect} (known: #{Entry::REGISTRY.keys.join(", ")})")
32
- klass.from_raw(common, raw)
33
- end
34
-
35
- # Parse the optional `source:` block. Returns nil when absent (workflow
36
- # produced entries register their produce logic in .textus/workflows/).
37
- def self.parse_source(raw, _key)
38
- block = raw["source"]
39
- return nil if block.nil?
40
-
41
- Textus::Manifest::Policy::Source.new(block)
42
- end
43
-
44
- # ADR 0094: `publish:` is a LIST of target objects — to-targets
45
- # [{to, template?, inject_boot?}] and/or a tree-target [{tree}]. The
46
- # ADR-0052 map forms ({to: […]} / {tree: …}) are retired.
47
- def self.publish_targets(raw)
48
- block = raw["publish"]
49
- return [] if block.nil?
50
-
51
- unless block.is_a?(Array)
52
- raise BadManifest.new(
53
- "entry '#{raw["key"]}': `publish:` must be a list of targets " \
54
- "[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
55
- )
56
- end
57
- block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
58
- end
59
-
60
- def self.resolve_format(raw, path)
61
- declared = raw["format"]
62
-
63
- return declared || "markdown" if path.nil? || path.empty?
64
-
65
- ext = File.extname(path)
66
- inferred = Textus::Format.infer_from_extension(ext)
67
-
68
- if declared.nil?
69
- return inferred if inferred
70
-
71
- return "markdown"
72
- end
73
-
74
- raise UsageError.new("entry '#{raw["key"]}': unknown format #{declared.inspect}") unless Textus::Format.formats.include?(declared)
75
-
76
- if ext != "" && inferred && inferred != declared
77
- raise UsageError.new(
78
- "entry '#{raw["key"]}': path extension #{ext.inspect} does not match declared format #{declared.inspect}",
79
- )
80
- end
81
-
82
- declared
83
- end
84
-
85
- # Derives the manifest-relative path from key + kind + format.
86
- # Key::Path.normalize_relative_path will prepend data/ at resolution time.
87
- def self.derive_path(key, kind_sym, format)
88
- dir_path = key.split(".").join("/")
89
- return dir_path if kind_sym == :nested
90
-
91
- ext = Textus::Format.for(format).extensions.first
92
- "#{dir_path}#{ext}"
93
- end
94
-
95
- private_class_method :derive_path
96
- end
97
- end
98
- end
99
- end
@@ -1,36 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- # A produced entry — `kind: produced` with an optional `source:` block.
5
- # When `source:` is present it must be `from: external` (out-of-band
6
- # generator; textus detects drift but never runs it). When absent the
7
- # entry is produced by a workflow file in .textus/workflows/.
8
- class Produced < Base
9
- attr_reader :source
10
-
11
- def initialize(source:, **rest)
12
- super(**rest)
13
- @source = source
14
- end
15
-
16
- def external? = @source&.external? || false
17
- def nested? = !!@raw["nested"]
18
-
19
- KIND = :produced
20
-
21
- # Publish existing store bytes via the shared publish mode (Publish::ToPaths
22
- # or Publish::None). Workflow runners handle the produce step; this method
23
- # only publishes whatever bytes are already on disk.
24
- def publish_via(pctx, prefix: nil)
25
- publish_mode.publish(pctx, prefix: prefix)
26
- end
27
-
28
- def self.from_raw(common, raw)
29
- new(source: Parser.parse_source(raw, common[:key]), **common)
30
- end
31
-
32
- Entry::REGISTRY[KIND] = self
33
- end
34
- end
35
- end
36
- end
@@ -1,45 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Publish
5
- # Base for every publish mode: wraps the resolved entry and owns the one
6
- # repo-root escape guard the writing modes share (ADR 0049). Subclasses
7
- # implement `#publish(pctx, prefix:)` returning the existing
8
- # `{ kind:, value:, pruned: }` shape (or nil), and `#validate!` for the
9
- # per-mode shape rules reached *because* this mode resolved.
10
- class Mode
11
- def initialize(entry)
12
- @entry = entry
13
- end
14
-
15
- attr_reader :entry
16
-
17
- # No shape rules by default — ToPaths/None publish without templating.
18
- def validate!; end
19
-
20
- # Whether this entry's subtree files are opaque payload that must
21
- # never be enumerated as keys. Only Tree (publish_tree, ADR 0047)
22
- # overrides to true; doctor's IllegalKeys and the resolver consult
23
- # this so they stop key-walking a keyless mirror's files.
24
- def keyless? = false
25
-
26
- private
27
-
28
- # Expand `rel` under repo_root and confirm it stays inside it.
29
- def repo_abs(pctx, rel)
30
- File.expand_path(File.join(pctx.repo_root, rel))
31
- end
32
-
33
- def inside_repo?(pctx, abs)
34
- abs.start_with?(File.expand_path(pctx.repo_root) + File::SEPARATOR)
35
- end
36
-
37
- # Store-side directory this entry's tree lives under.
38
- def store_base(pctx)
39
- File.join(pctx.root, Textus::Key::Path.normalize_relative_path(entry.path))
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,14 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Publish
5
- # An entry with no publish_* key — nothing to publish.
6
- class None < Mode
7
- def publish(_pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
8
- nil
9
- end
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,73 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Publish
5
- # ADR 0049: the one walk->publish->prune pipeline behind Tree (whole-entry
6
- # mirror, ADR 0047). It was once shared with the per-leaf publish_each
7
- # mode too; ADR 0051 removed publish_each, leaving Tree the only caller.
8
- # The `walk_root`/`prune_honors_ignore:` parameters survive from that
9
- # shared shape — Tree always walks at `base` and honors `ignore` in the
10
- # prune (ADR 0047 D4, so a derived index in the mirrored dir survives).
11
- class SubtreeMirror
12
- def initialize(entry, pctx, publisher: Textus::Port::Publisher.new)
13
- @entry = entry
14
- @pctx = pctx
15
- @publisher = publisher
16
- end
17
-
18
- # base: store dir the entry owns — the root `ignored?` globs are
19
- # relative to (ADR 0042).
20
- # walk_root: dir the glob is rooted at (== base for Tree). dst paths
21
- # mirror rel-to-walk_root.
22
- # target_dir: repo-side destination root.
23
- # key/envelope: emitted per file; envelope is nil for the keyless Tree.
24
- # prune_honors_ignore: when true a managed file the entry `ignore`s
25
- # survives the prune (ADR 0047 D4 — lets a derived index live in the
26
- # mirrored dir); when false every unwritten managed file is pruned.
27
- def mirror(base:, walk_root:, target_dir:, key:, envelope:, prune_honors_ignore:)
28
- return { written: [], pruned: [] } unless File.directory?(walk_root)
29
-
30
- written = publish_files(base: base, walk_root: walk_root, target_dir: target_dir, key: key, envelope: envelope)
31
- { written: written, pruned: prune(target_dir, written, prune_honors_ignore) }
32
- end
33
-
34
- private
35
-
36
- def publish_files(base:, walk_root:, target_dir:, key:, envelope:)
37
- # FNM_DOTMATCH includes dotfiles; File.file? below skips dirs (and
38
- # symlinks-to-dirs). Trees are authored content, not symlink graphs.
39
- Dir.glob(File.join(walk_root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |src|
40
- next nil unless File.file?(src)
41
- next nil if @entry.ignored?(relative(src, base))
42
-
43
- dst = File.join(target_dir, relative(src, walk_root))
44
- @publisher.publish(source: src, target: dst, store_root: @pctx.root)
45
- @pctx.emit(:entry_published, key: key, envelope: envelope, source: src, target: dst)
46
- { "key" => key, "source" => src, "target" => dst }
47
- end
48
- end
49
-
50
- # Scoped to target_dir. Safe across leaves because ADR 0046 D5
51
- # (shallowest-index-wins) keeps leaf target dirs non-nesting, so
52
- # targets_under can't reach another leaf's sentinels.
53
- def prune(target_dir, written, honor_ignore)
54
- kept = written.map { |w| File.expand_path(w["target"]) }
55
- store = Textus::Port::SentinelStore.new
56
- store.targets_under(target_dir, @pctx.root).filter_map do |managed|
57
- abs = File.expand_path(managed)
58
- next nil if kept.include?(abs)
59
- next nil if honor_ignore && @entry.ignored?(relative(abs, target_dir))
60
-
61
- @publisher.unpublish(target: managed, store_root: @pctx.root)
62
- managed
63
- end
64
- end
65
-
66
- def relative(path, root)
67
- path.sub(%r{\A#{Regexp.escape(root)}/}, "")
68
- end
69
- end
70
- end
71
- end
72
- end
73
- end
@@ -1,16 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Publish
5
- # Template-variable detection for publish targets. The only surviving
6
- # use after ADR 0051 (which removed publish_each and its `{leaf}`/
7
- # `{basename}`/`{key}`/`{ext}` vocabulary) is Tree.validate!, which uses
8
- # VAR_RE to reject any `{var}` in a publish_tree value — that key names a
9
- # single directory by plain path and interprets no variables.
10
- module Template
11
- VAR_RE = /\{([a-z]+)\}/
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,76 +0,0 @@
1
- require "tempfile"
2
-
3
- module Textus
4
- class Manifest
5
- class Entry
6
- module Publish
7
- # publish.to: render or copy the entry's stored data to each fixed repo path.
8
- # The behaviour of any entry that declares `publish: [{ to: ... }, ...]`.
9
- # ADR 0094: iterates publish_targets (to-targets), rendering through a
10
- # template when the target declares one, or copying verbatim otherwise.
11
- class ToPaths < Mode
12
- def initialize(entry, publisher: Textus::Port::Publisher.new)
13
- super(entry)
14
- @publisher = publisher
15
- end
16
-
17
- def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
18
- targets = entry.publish_targets.select(&:to_target?)
19
-
20
- return nil if targets.empty?
21
-
22
- data_path = pctx.manifest.resolver.resolve(entry.key).path
23
- envelope = pctx.reader.call(entry.key)
24
- renderer = Textus::Produce::Render.new(template_loader: ->(n) { pctx.read_template(n) })
25
- content = nil # parsed lazily; the data's `content` (always _meta-free)
26
-
27
- targets.each do |t|
28
- if t.renders?
29
- content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
30
- publish_bytes(render_bytes(t, content, renderer, pctx), entry.key, t, pctx, data_path, envelope)
31
- elsif strip_meta?(entry)
32
- content ||= Textus::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
33
- bytes = Textus::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
34
- publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
35
- else
36
- # opaque / command / non-structured — publish the stored file as-is
37
- target_abs = File.join(pctx.repo_root, t.to)
38
- @publisher.publish(source: data_path, target: target_abs, store_root: pctx.root)
39
- pctx.emit(:entry_published, key: entry.key, envelope: envelope, source: data_path, target: target_abs)
40
- end
41
- end
42
-
43
- { kind: :built, value: { "key" => entry.key, "path" => data_path, "published_to" => targets.map(&:to) } }
44
- end
45
-
46
- private
47
-
48
- def strip_meta?(entry)
49
- %w[json yaml].include?(entry.format.to_s)
50
- end
51
-
52
- def render_bytes(target, content, renderer, pctx)
53
- boot = target.inject_boot ? Textus::Boot.build(container: pctx.container) : nil
54
- renderer.bytes_for(target: target, data: content, boot: boot)
55
- end
56
-
57
- # Write bytes to a system temp, publish (recording the persistent data
58
- # file as the sentinel source), then remove the temp — the store is
59
- # never polluted with render artifacts.
60
- def publish_bytes(bytes, key, target, pctx, data_path, envelope)
61
- target_abs = File.join(pctx.repo_root, target.to)
62
- Tempfile.create(["textus-publish", File.extname(target.to)]) do |f|
63
- f.binmode
64
- f.write(bytes)
65
- f.flush
66
- @publisher.publish(
67
- source: f.path, target: target_abs, store_root: pctx.root, provenance_source: data_path,
68
- )
69
- end
70
- pctx.emit(:entry_published, key: key, envelope: envelope, source: data_path, target: target_abs)
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end
@@ -1,50 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- module Publish
5
- # publish_tree (ADR 0047): mirror this entry's whole subtree to one
6
- # target dir by real path. No resolver, no keys — files are opaque
7
- # payload (envelope nil). The prune honors `ignore` so a derived index
8
- # (e.g. a SKILL.md written by a separate entry into the same dir)
9
- # survives the whole-target prune (ADR 0047 D4).
10
- class Tree < Mode
11
- # Mirrored files are opaque payload, never addressable keys (ADR 0047).
12
- def keyless? = true
13
-
14
- def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
15
- target_rel = entry.publish_tree
16
- target_dir = repo_abs(pctx, target_rel)
17
- unless inside_repo?(pctx, target_dir)
18
- raise Textus::PublishError.new(
19
- "entry '#{entry.key}': publish_tree target '#{target_rel}' escapes repo root",
20
- )
21
- end
22
-
23
- result = SubtreeMirror.new(entry, pctx).mirror(
24
- base: store_base(pctx),
25
- walk_root: store_base(pctx),
26
- target_dir: target_dir,
27
- key: entry.key,
28
- envelope: nil,
29
- prune_honors_ignore: true,
30
- )
31
- { kind: :leaves, value: result[:written], pruned: result[:pruned] }
32
- end
33
-
34
- def validate!
35
- publish_tree = entry.publish_tree
36
- raise UsageError.new("entry '#{entry.key}': publish.tree must be a string") unless publish_tree.is_a?(String)
37
-
38
- used_vars = publish_tree.scan(Template::VAR_RE).flatten
39
- return if used_vars.empty?
40
-
41
- raise UsageError.new(
42
- "entry '#{entry.key}': publish.tree names a single directory and takes no template variable(s) " \
43
- "#{used_vars.map { |v| "{#{v}}" }.join(", ")} — it mirrors the whole subtree to one target dir.",
44
- )
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,45 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Entry
4
- # ADR 0049: the publish design is a key-split concept (ADR 0047 table)
5
- # realized as one resolved sum type. Each directory entry resolves, once,
6
- # to one Publish::* mode that owns its publish algorithm — no nil-cascade,
7
- # no pairwise exclusivity guards, one shared subtree mirror. ADR 0051
8
- # removed `publish_each` (both leaf modes); ADR 0052 folded the two surviving
9
- # keys into one `publish:` block (`to:` xor `tree:`). The surface is two modes:
10
- #
11
- # None — nothing to publish (no publish: block)
12
- # ToPaths — publish: { to: [...] } — 1 stored file -> N fixed repo paths
13
- # Tree — publish: { tree: "dir" } — whole entry subtree -> 1 dir, no keys
14
- module Publish
15
- # Resolve an entry to its single publish mode. The publish config is the
16
- # ADR 0052 `publish:` block, sourced into entry.publish_to/publish_tree.
17
- # Raises one UsageError if both `publish.to` and `publish.tree` are set —
18
- # the block groups the two but does not make exclusivity structural, so
19
- # this stays the one enforcement point (ADR 0052 D2).
20
- def self.resolve(entry)
21
- set = []
22
- set << "publish.to" unless Array(entry.publish_to).empty?
23
- set << "publish.tree" unless entry.publish_tree.nil?
24
-
25
- if set.length > 1
26
- raise Textus::UsageError.new(
27
- "entry '#{entry.key}': #{set.join(" and ")} are mutually exclusive — an entry publishes exactly one way",
28
- )
29
- end
30
-
31
- mode_for(entry, set.first)
32
- end
33
-
34
- def self.mode_for(entry, key)
35
- case key
36
- when "publish.to" then ToPaths.new(entry)
37
- when "publish.tree" then Tree.new(entry)
38
- else None.new(entry)
39
- end
40
- end
41
- private_class_method :mode_for
42
- end
43
- end
44
- end
45
- end