textus 0.55.1 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -2768
  3. data/README.md +17 -18
  4. data/lib/textus/artifact.rb +8 -0
  5. data/lib/textus/errors.rb +2 -39
  6. data/lib/textus/human.rb +7 -0
  7. data/lib/textus/infra/adapters/concurrency_adapter.rb +15 -0
  8. data/lib/textus/infra/clock.rb +7 -0
  9. data/lib/textus/infra/database.rb +127 -0
  10. data/lib/textus/infra/file_system.rb +54 -0
  11. data/lib/textus/infra/locks.rb +50 -0
  12. data/lib/textus/infra/port/clock.rb +18 -0
  13. data/lib/textus/infra/port/publisher.rb +71 -0
  14. data/lib/textus/infra/port/sentinel.rb +25 -0
  15. data/lib/textus/infra/port/sentinel_store.rb +93 -0
  16. data/lib/textus/infra/store/audit_log.rb +42 -0
  17. data/lib/textus/infra/store/base.rb +25 -0
  18. data/lib/textus/infra/store/cursor_store.rb +27 -0
  19. data/lib/textus/infra/store/entry_index.rb +31 -0
  20. data/lib/textus/infra/store/envelope/meta.rb +63 -0
  21. data/lib/textus/infra/store/file_store.rb +23 -0
  22. data/lib/textus/infra/store/index/builder.rb +28 -0
  23. data/lib/textus/infra/store/index/lookup.rb +23 -0
  24. data/lib/textus/infra/store/link_store.rb +43 -0
  25. data/lib/textus/infra/store/workflow_queue.rb +54 -0
  26. data/lib/textus/infra/store.rb +29 -0
  27. data/lib/textus/ingest.rb +8 -0
  28. data/lib/textus/lanes/artifact/cache.rb +35 -0
  29. data/lib/textus/lanes/artifact/handlers.rb +29 -0
  30. data/lib/textus/lanes/artifact/lifecycle.rb +23 -0
  31. data/lib/textus/lanes/artifact/verbs.rb +18 -0
  32. data/lib/textus/lanes/ingest/dedup.rb +17 -0
  33. data/lib/textus/lanes/ingest/entry_types/asset.rb +45 -0
  34. data/lib/textus/lanes/ingest/entry_types/link.rb +16 -0
  35. data/lib/textus/lanes/ingest/entry_types/text.rb +17 -0
  36. data/lib/textus/lanes/ingest/entry_types.rb +25 -0
  37. data/lib/textus/lanes/ingest/handlers.rb +54 -0
  38. data/lib/textus/lanes/ingest/index_rebuilder.rb +13 -0
  39. data/lib/textus/lanes/ingest/key_builder.rb +33 -0
  40. data/lib/textus/lanes/ingest/resolver.rb +21 -0
  41. data/lib/textus/lanes/knowledge/freeform.rb +15 -0
  42. data/lib/textus/lanes/knowledge/handlers.rb +53 -0
  43. data/lib/textus/lanes/knowledge/runner.rb +45 -0
  44. data/lib/textus/lanes/knowledge/verbs.rb +16 -0
  45. data/lib/textus/lanes/scratchpad/handlers.rb +164 -0
  46. data/lib/textus/lanes/scratchpad/verbs.rb +73 -0
  47. data/lib/textus/{gate → protocol}/binder.rb +2 -10
  48. data/lib/textus/protocol/boot/catalog.rb +34 -0
  49. data/lib/textus/protocol/boot/context.rb +124 -0
  50. data/lib/textus/protocol/boot/protocol.rb +33 -0
  51. data/lib/textus/protocol/boot.rb +42 -0
  52. data/lib/textus/protocol/builder/domain.rb +17 -0
  53. data/lib/textus/protocol/builder/infrastructure.rb +17 -0
  54. data/lib/textus/protocol/builder/runtime.rb +46 -0
  55. data/lib/textus/protocol/builder.rb +11 -0
  56. data/lib/textus/protocol/container.rb +22 -0
  57. data/lib/textus/protocol/diff.rb +170 -0
  58. data/lib/textus/protocol/entry_constraint.rb +54 -0
  59. data/lib/textus/protocol/envelope.rb +70 -0
  60. data/lib/textus/protocol/format/base.rb +80 -0
  61. data/lib/textus/protocol/format/bash.rb +10 -0
  62. data/lib/textus/protocol/format/javascript.rb +10 -0
  63. data/lib/textus/protocol/format/json.rb +82 -0
  64. data/lib/textus/protocol/format/markdown.rb +53 -0
  65. data/lib/textus/protocol/format/python.rb +10 -0
  66. data/lib/textus/protocol/format/ruby.rb +10 -0
  67. data/lib/textus/protocol/format/script.rb +55 -0
  68. data/lib/textus/protocol/format/text.rb +48 -0
  69. data/lib/textus/protocol/format/yaml.rb +105 -0
  70. data/lib/textus/protocol/format.rb +73 -0
  71. data/lib/textus/protocol/freshness/ttl_evaluator.rb +61 -0
  72. data/lib/textus/protocol/gate.rb +61 -0
  73. data/lib/textus/protocol/handlers/command_context.rb +19 -0
  74. data/lib/textus/protocol/handlers/concern.rb +15 -0
  75. data/lib/textus/protocol/handlers/ops/audit_filter.rb +37 -0
  76. data/lib/textus/protocol/handlers/ops/bulk_planner.rb +38 -0
  77. data/lib/textus/protocol/handlers/ops/data_mv.rb +49 -0
  78. data/lib/textus/protocol/handlers/ops/git_blame.rb +65 -0
  79. data/lib/textus/protocol/handlers/ops/pulse_coordinator.rb +37 -0
  80. data/lib/textus/protocol/handlers/ops/source_expander.rb +41 -0
  81. data/lib/textus/protocol/handlers/query_context.rb +41 -0
  82. data/lib/textus/protocol/handlers/read.rb +67 -0
  83. data/lib/textus/protocol/handlers/system.rb +93 -0
  84. data/lib/textus/protocol/handlers/write.rb +57 -0
  85. data/lib/textus/protocol/init.rb +143 -0
  86. data/lib/textus/protocol/key/grammar.rb +35 -0
  87. data/lib/textus/protocol/key/matching.rb +23 -0
  88. data/lib/textus/protocol/key/path.rb +23 -0
  89. data/lib/textus/protocol/key.rb +25 -0
  90. data/lib/textus/protocol/lane.rb +12 -0
  91. data/lib/textus/{store/geometry.rb → protocol/layout.rb} +32 -22
  92. data/lib/textus/protocol/links/resolver.rb +33 -0
  93. data/lib/textus/protocol/links/uri_rewriter.rb +30 -0
  94. data/lib/textus/protocol/links.rb +10 -0
  95. data/lib/textus/protocol/manifest/data.rb +90 -0
  96. data/lib/textus/protocol/manifest/entry/base.rb +95 -0
  97. data/lib/textus/protocol/manifest/entry/leaf.rb +19 -0
  98. data/lib/textus/protocol/manifest/entry/nested.rb +19 -0
  99. data/lib/textus/protocol/manifest/entry/parser.rb +71 -0
  100. data/lib/textus/protocol/manifest/entry/publish.rb +209 -0
  101. data/lib/textus/protocol/manifest/entry.rb +10 -0
  102. data/lib/textus/protocol/manifest/policy/publish_target.rb +36 -0
  103. data/lib/textus/protocol/manifest/policy.rb +69 -0
  104. data/lib/textus/protocol/manifest/resolver.rb +156 -0
  105. data/lib/textus/protocol/manifest/schema/contract.rb +53 -0
  106. data/lib/textus/protocol/manifest/schema/validator.rb +52 -0
  107. data/lib/textus/protocol/manifest/schema.rb +40 -0
  108. data/lib/textus/protocol/manifest.rb +62 -0
  109. data/lib/textus/protocol/produce/render/context.rb +43 -0
  110. data/lib/textus/protocol/produce/render.rb +44 -0
  111. data/lib/textus/protocol/schema/registry.rb +44 -0
  112. data/lib/textus/protocol/schema/tools.rb +104 -0
  113. data/lib/textus/protocol/schema.rb +117 -0
  114. data/lib/textus/protocol/session/bridge.rb +11 -0
  115. data/lib/textus/protocol/session/context.rb +5 -0
  116. data/lib/textus/protocol/session.rb +87 -0
  117. data/lib/textus/protocol/store_engine/delete.rb +37 -0
  118. data/lib/textus/protocol/store_engine/move.rb +109 -0
  119. data/lib/textus/protocol/store_engine/pipeline.rb +61 -0
  120. data/lib/textus/protocol/store_engine/put.rb +104 -0
  121. data/lib/textus/protocol/store_engine.rb +86 -0
  122. data/lib/textus/protocol/v4.rb +25 -0
  123. data/lib/textus/protocol/verb_registry/arg_spec.rb +19 -0
  124. data/lib/textus/protocol/verb_registry/core_verbs.rb +164 -0
  125. data/lib/textus/protocol/verb_registry/verb_spec.rb +42 -0
  126. data/lib/textus/protocol/verb_registry.rb +42 -0
  127. data/lib/textus/surface/cli/group/{rule.rb → web.rb} +2 -2
  128. data/lib/textus/surface/cli/runner.rb +25 -25
  129. data/lib/textus/surface/cli/sources.rb +1 -3
  130. data/lib/textus/surface/cli/verb/get.rb +3 -6
  131. data/lib/textus/surface/cli/verb/init.rb +1 -1
  132. data/lib/textus/surface/cli/verb/put.rb +8 -6
  133. data/lib/textus/surface/cli/verb/web_serve.rb +22 -0
  134. data/lib/textus/surface/cli.rb +25 -6
  135. data/lib/textus/surface/dispatch.rb +17 -0
  136. data/lib/textus/surface/mcp/adapters/mcp_adapter.rb +28 -0
  137. data/lib/textus/surface/mcp/catalog.rb +26 -62
  138. data/lib/textus/surface/mcp/cursor_manager.rb +22 -0
  139. data/lib/textus/surface/mcp/drift_annotation.rb +19 -0
  140. data/lib/textus/surface/mcp/errors.rb +0 -10
  141. data/lib/textus/surface/mcp/server.rb +46 -46
  142. data/lib/textus/surface/watcher.rb +18 -12
  143. data/lib/textus/surface/web.rb +50 -0
  144. data/lib/textus/value/call.rb +1 -1
  145. data/lib/textus/{core → value}/duration.rb +1 -4
  146. data/lib/textus/value/etag.rb +7 -7
  147. data/lib/textus/value/payload.rb +7 -0
  148. data/lib/textus/version.rb +1 -1
  149. data/lib/textus/workflow/consumer.rb +39 -0
  150. data/lib/textus/workflow/dsl/lifecycle.rb +32 -0
  151. data/lib/textus/workflow/dsl/pattern_matcher.rb +20 -0
  152. data/lib/textus/workflow/dsl/step_builder.rb +56 -0
  153. data/lib/textus/workflow/dsl.rb +111 -21
  154. data/lib/textus/workflow/event_emitter.rb +21 -0
  155. data/lib/textus/workflow/publisher.rb +59 -0
  156. data/lib/textus/workflow/registry.rb +31 -8
  157. data/lib/textus/workflow/runner.rb +25 -61
  158. data/lib/textus/workflow/scheduler.rb +45 -0
  159. data/lib/textus/workflow/step_executor.rb +78 -0
  160. data/lib/textus/workflow/step_helpers.rb +102 -0
  161. data/lib/textus/workflow.rb +2 -5
  162. data/lib/textus.rb +16 -80
  163. metadata +167 -183
  164. data/SPEC.md +0 -908
  165. data/docs/architecture/README.md +0 -273
  166. data/docs/reference/conventions.md +0 -138
  167. data/lib/textus/action/accept.rb +0 -46
  168. data/lib/textus/action/audit.rb +0 -94
  169. data/lib/textus/action/base.rb +0 -42
  170. data/lib/textus/action/blame.rb +0 -79
  171. data/lib/textus/action/boot.rb +0 -15
  172. data/lib/textus/action/data_mv.rb +0 -58
  173. data/lib/textus/action/deps.rb +0 -19
  174. data/lib/textus/action/doctor.rb +0 -17
  175. data/lib/textus/action/drain.rb +0 -31
  176. data/lib/textus/action/enqueue.rb +0 -37
  177. data/lib/textus/action/get.rb +0 -34
  178. data/lib/textus/action/ingest.rb +0 -199
  179. data/lib/textus/action/jobs.rb +0 -27
  180. data/lib/textus/action/key_delete.rb +0 -26
  181. data/lib/textus/action/key_delete_prefix.rb +0 -35
  182. data/lib/textus/action/key_mv.rb +0 -122
  183. data/lib/textus/action/key_mv_prefix.rb +0 -48
  184. data/lib/textus/action/list.rb +0 -28
  185. data/lib/textus/action/propose.rb +0 -42
  186. data/lib/textus/action/published.rb +0 -22
  187. data/lib/textus/action/pulse.rb +0 -49
  188. data/lib/textus/action/put.rb +0 -38
  189. data/lib/textus/action/rdeps.rb +0 -24
  190. data/lib/textus/action/reject.rb +0 -28
  191. data/lib/textus/action/rule_explain.rb +0 -81
  192. data/lib/textus/action/rule_lint.rb +0 -62
  193. data/lib/textus/action/rule_list.rb +0 -38
  194. data/lib/textus/action/schema_envelope.rb +0 -22
  195. data/lib/textus/action/uid.rb +0 -19
  196. data/lib/textus/action/where.rb +0 -21
  197. data/lib/textus/boot.rb +0 -202
  198. data/lib/textus/contract/arg.rb +0 -10
  199. data/lib/textus/contract/dsl.rb +0 -88
  200. data/lib/textus/contract/spec.rb +0 -25
  201. data/lib/textus/contract.rb +0 -12
  202. data/lib/textus/core/freshness/evaluator.rb +0 -150
  203. data/lib/textus/core/freshness/verdict.rb +0 -34
  204. data/lib/textus/core/freshness.rb +0 -11
  205. data/lib/textus/core/retention/sweep.rb +0 -57
  206. data/lib/textus/core/retention.rb +0 -11
  207. data/lib/textus/core/sentinel.rb +0 -30
  208. data/lib/textus/doctor/check/audit_log.rb +0 -34
  209. data/lib/textus/doctor/check/generator_drift.rb +0 -29
  210. data/lib/textus/doctor/check/illegal_keys.rb +0 -60
  211. data/lib/textus/doctor/check/manifest_files.rb +0 -25
  212. data/lib/textus/doctor/check/notebook_sources.rb +0 -50
  213. data/lib/textus/doctor/check/orphaned_publish_targets.rb +0 -35
  214. data/lib/textus/doctor/check/proposal_targets.rb +0 -45
  215. data/lib/textus/doctor/check/protocol_version.rb +0 -47
  216. data/lib/textus/doctor/check/publish_tree_index_overlap.rb +0 -48
  217. data/lib/textus/doctor/check/raw_asset_paths.rb +0 -50
  218. data/lib/textus/doctor/check/rule_ambiguity.rb +0 -49
  219. data/lib/textus/doctor/check/schema_parse_error.rb +0 -28
  220. data/lib/textus/doctor/check/schema_violations.rb +0 -28
  221. data/lib/textus/doctor/check/schemas.rb +0 -26
  222. data/lib/textus/doctor/check/sentinels.rb +0 -60
  223. data/lib/textus/doctor/check/stale_reviewed_stamp.rb +0 -54
  224. data/lib/textus/doctor/check/templates.rb +0 -29
  225. data/lib/textus/doctor/check/unowned_schema_fields.rb +0 -40
  226. data/lib/textus/doctor/check.rb +0 -41
  227. data/lib/textus/doctor/validator.rb +0 -93
  228. data/lib/textus/doctor.rb +0 -54
  229. data/lib/textus/format/base.rb +0 -49
  230. data/lib/textus/format/json.rb +0 -102
  231. data/lib/textus/format/markdown.rb +0 -80
  232. data/lib/textus/format/shared.rb +0 -17
  233. data/lib/textus/format/text.rb +0 -55
  234. data/lib/textus/format/yaml.rb +0 -125
  235. data/lib/textus/format.rb +0 -46
  236. data/lib/textus/gate/auth.rb +0 -212
  237. data/lib/textus/gate.rb +0 -92
  238. data/lib/textus/init.rb +0 -157
  239. data/lib/textus/jobs.rb +0 -9
  240. data/lib/textus/key/distance.rb +0 -55
  241. data/lib/textus/key/grammar.rb +0 -33
  242. data/lib/textus/key/matching.rb +0 -24
  243. data/lib/textus/key/path.rb +0 -28
  244. data/lib/textus/manifest/capabilities.rb +0 -29
  245. data/lib/textus/manifest/data.rb +0 -102
  246. data/lib/textus/manifest/entry/base.rb +0 -105
  247. data/lib/textus/manifest/entry/ignore_matcher.rb +0 -46
  248. data/lib/textus/manifest/entry/leaf.rb +0 -17
  249. data/lib/textus/manifest/entry/nested.rb +0 -37
  250. data/lib/textus/manifest/entry/parser.rb +0 -99
  251. data/lib/textus/manifest/entry/produced.rb +0 -36
  252. data/lib/textus/manifest/entry/publish/mode.rb +0 -45
  253. data/lib/textus/manifest/entry/publish/none.rb +0 -14
  254. data/lib/textus/manifest/entry/publish/subtree_mirror.rb +0 -73
  255. data/lib/textus/manifest/entry/publish/template.rb +0 -16
  256. data/lib/textus/manifest/entry/publish/to_paths.rb +0 -76
  257. data/lib/textus/manifest/entry/publish/tree.rb +0 -50
  258. data/lib/textus/manifest/entry/publish.rb +0 -45
  259. data/lib/textus/manifest/entry/validators/format_matrix.rb +0 -21
  260. data/lib/textus/manifest/entry/validators/ignore.rb +0 -28
  261. data/lib/textus/manifest/entry/validators/publish.rb +0 -30
  262. data/lib/textus/manifest/entry/validators.rb +0 -18
  263. data/lib/textus/manifest/entry.rb +0 -8
  264. data/lib/textus/manifest/policy/matcher.rb +0 -51
  265. data/lib/textus/manifest/policy/publish_target.rb +0 -34
  266. data/lib/textus/manifest/policy/react.rb +0 -30
  267. data/lib/textus/manifest/policy/retention.rb +0 -29
  268. data/lib/textus/manifest/policy/source.rb +0 -30
  269. data/lib/textus/manifest/policy.rb +0 -95
  270. data/lib/textus/manifest/resolver.rb +0 -118
  271. data/lib/textus/manifest/rules.rb +0 -86
  272. data/lib/textus/manifest/schema/contract.rb +0 -61
  273. data/lib/textus/manifest/schema/keys.rb +0 -90
  274. data/lib/textus/manifest/schema/semantics/cross_field.rb +0 -53
  275. data/lib/textus/manifest/schema/semantics/invariants.rb +0 -125
  276. data/lib/textus/manifest/schema/semantics/migration.rb +0 -83
  277. data/lib/textus/manifest/schema/semantics.rb +0 -27
  278. data/lib/textus/manifest/schema/validator.rb +0 -48
  279. data/lib/textus/manifest/schema/vocabulary.rb +0 -25
  280. data/lib/textus/manifest/schema.rb +0 -30
  281. data/lib/textus/manifest.rb +0 -65
  282. data/lib/textus/meta.rb +0 -54
  283. data/lib/textus/port/audit_log.rb +0 -268
  284. data/lib/textus/port/build_lock.rb +0 -64
  285. data/lib/textus/port/clock.rb +0 -10
  286. data/lib/textus/port/publisher.rb +0 -60
  287. data/lib/textus/port/sentinel_store.rb +0 -83
  288. data/lib/textus/port/storage/file_stat.rb +0 -19
  289. data/lib/textus/port/storage/file_store.rb +0 -26
  290. data/lib/textus/port/store.rb +0 -93
  291. data/lib/textus/port/watcher_lock.rb +0 -48
  292. data/lib/textus/produce/engine.rb +0 -50
  293. data/lib/textus/produce/render.rb +0 -20
  294. data/lib/textus/schema/tools.rb +0 -115
  295. data/lib/textus/schema.rb +0 -100
  296. data/lib/textus/schemas.rb +0 -54
  297. data/lib/textus/store/compositor.rb +0 -34
  298. data/lib/textus/store/container.rb +0 -43
  299. data/lib/textus/store/cursor.rb +0 -26
  300. data/lib/textus/store/envelope/reader.rb +0 -43
  301. data/lib/textus/store/envelope/writer.rb +0 -195
  302. data/lib/textus/store/index/builder.rb +0 -74
  303. data/lib/textus/store/index/lookup.rb +0 -60
  304. data/lib/textus/store/jobs/base.rb +0 -13
  305. data/lib/textus/store/jobs/index.rb +0 -15
  306. data/lib/textus/store/jobs/materialize.rb +0 -15
  307. data/lib/textus/store/jobs/plan.rb +0 -11
  308. data/lib/textus/store/jobs/planner.rb +0 -104
  309. data/lib/textus/store/jobs/queue.rb +0 -154
  310. data/lib/textus/store/jobs/registry.rb +0 -19
  311. data/lib/textus/store/jobs/retention.rb +0 -50
  312. data/lib/textus/store/jobs/sweep.rb +0 -21
  313. data/lib/textus/store/jobs/worker.rb +0 -64
  314. data/lib/textus/store/session.rb +0 -37
  315. data/lib/textus/store.rb +0 -99
  316. data/lib/textus/surface/cli/verb/doctor.rb +0 -19
  317. data/lib/textus/surface/cli/verb/schema_diff.rb +0 -17
  318. data/lib/textus/surface/cli/verb/schema_init.rb +0 -21
  319. data/lib/textus/surface/cli/verb/schema_migrate.rb +0 -21
  320. data/lib/textus/surface/mcp.rb +0 -8
  321. data/lib/textus/surface/projector.rb +0 -27
  322. data/lib/textus/surface/role_scope.rb +0 -34
  323. data/lib/textus/value/command.rb +0 -16
  324. data/lib/textus/value/envelope.rb +0 -89
  325. data/lib/textus/value/result.rb +0 -26
  326. data/lib/textus/workflow/collector.rb +0 -27
  327. data/lib/textus/workflow/context.rb +0 -5
  328. data/lib/textus/workflow/loader.rb +0 -17
  329. data/lib/textus/workflow/pattern.rb +0 -18
@@ -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,Metrics/AbcSize
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
@@ -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,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::Core::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
@@ -1,118 +0,0 @@
1
- module Textus
2
- class Manifest
3
- class Resolver
4
- Resolution = ::Data.define(:entry, :path, :remaining)
5
-
6
- def initialize(data)
7
- @data = data
8
- end
9
-
10
- def resolve(key)
11
- @data.validate_key!(key)
12
- segments = key.split(".")
13
- candidates = @data.entries
14
- .map { |e| [e, e.key.split(".")] }
15
- .select { |(_, esegs)| esegs == segments[0, esegs.length] }
16
- .sort_by { |(_, esegs)| -esegs.length }
17
- raise UnknownKey.new(key, suggestions: suggestions_for(key)) if candidates.empty?
18
-
19
- entry, esegs = candidates.first
20
- remaining = segments[esegs.length..]
21
- build_resolution(entry, remaining, key)
22
- end
23
-
24
- def suggestions_for(key)
25
- candidates = enumerate.map { |r| r[:key] }
26
- candidates.concat(@data.entries.reject { |e| nested_entry?(e) }.map(&:key))
27
- candidates.uniq!
28
- Key::Distance.suggest(key, candidates, limit: 5)
29
- rescue StandardError
30
- []
31
- end
32
-
33
- def enumerate(prefix: nil, include_keyless: false)
34
- out = @data.entries.flat_map do |entry|
35
- nested_entry?(entry) ? enumerate_nested(entry, include_keyless: include_keyless) : enumerate_leaf(entry)
36
- end
37
- out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
38
- out.sort_by { |row| row[:key] }
39
- end
40
-
41
- private
42
-
43
- # Returns true for entries that behave as nested (Nested subclass, or any
44
- # entry with nested: true in the raw YAML — e.g. Intake entries covering
45
- # a directory of leaf files).
46
- def nested_entry?(entry)
47
- entry.nested?
48
- end
49
-
50
- def build_resolution(entry, remaining, key)
51
- if remaining.empty?
52
- Resolution.new(entry: entry, path: resolve_leaf_path(entry), remaining: [])
53
- else
54
- raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless nested_entry?(entry)
55
-
56
- primary_ext = Textus::Format.for(entry.format).extensions.first
57
- base = Textus::Key::Path.normalize_relative_path(entry.path)
58
- path = File.join(@data.root, base, *remaining) + primary_ext
59
- Resolution.new(entry: entry, path: path, remaining: remaining)
60
- end
61
- end
62
-
63
- def enumerate_leaf(entry)
64
- fp = resolve_leaf_path(entry)
65
- File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
66
- end
67
-
68
- def enumerate_nested(entry, include_keyless: false)
69
- # publish_tree mirrors opaque payload by path — its files are never
70
- # enumerated as keys (ADR 0047). Ask the resolved mode, not the path.
71
- # The `include_keyless:` override is used only by the projection lister
72
- # so that `from: project` selects can read source data from keyless
73
- # nested entries (e.g. knowledge.decisions) without exposing them as
74
- # addressable store keys in the public `list` surface.
75
- return [] if entry.publish_mode.keyless? && !include_keyless
76
-
77
- base = File.join(@data.root, Textus::Key::Path.normalize_relative_path(entry.path))
78
- return [] unless File.directory?(base)
79
-
80
- Dir.glob(File.join(base, nested_glob(entry.format)))
81
- .filter_map { |path| nested_row_for(entry, base, path) }
82
- end
83
-
84
- def nested_row_for(entry, base, path)
85
- rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
86
- return nil if entry.ignored?(rel)
87
-
88
- stripped = rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
89
- segs = stripped.split("/").reject { |s| s.empty? || s == "." }
90
- return nil if segs.empty?
91
-
92
- illegal = segs.find { |s| !valid_segment?(s) }
93
- if illegal
94
- warn("textus: skipping illegal key segment '#{illegal}' at #{path} — " \
95
- "rename to match [a-z0-9][a-z0-9-]* (run 'textus doctor' for the full list)")
96
- return nil
97
- end
98
-
99
- { key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
100
- end
101
-
102
- def valid_segment?(seg)
103
- return false if seg.nil? || seg.empty?
104
- return false if seg.length > Key::Grammar::MAX_SEGMENT_LEN
105
-
106
- seg.match?(Key::Grammar::SEGMENT)
107
- end
108
-
109
- def resolve_leaf_path(entry)
110
- Textus::Key::Path.resolve(@data, entry)
111
- end
112
-
113
- def nested_glob(format)
114
- Textus::Format.for(format).nested_glob
115
- end
116
- end
117
- end
118
- end