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
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ # Namespace only — Resolver and UriRewriter are in links/.
4
+ # The real edge store is Textus::Infra::Store::LinkStore.
5
+ # rubocop:disable Lint/EmptyClass
6
+ class Links
7
+ end
8
+ # rubocop:enable Lint/EmptyClass
9
+ end
10
+ end
@@ -0,0 +1,90 @@
1
+ require_relative "schema"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Manifest
6
+ class Data
7
+ AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 1000 }.freeze
8
+ WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
9
+
10
+ attr_reader :raw, :root, :entries, :declared_lane_kinds,
11
+ :lane_descs,
12
+ :audit_config, :worker_config, :role_caps
13
+
14
+ def self.validate_key!(key)
15
+ raise UsageError.new("empty key") if key.nil? || key.empty?
16
+
17
+ Protocol::Key::Grammar.validate!(key)
18
+ end
19
+
20
+ def validate_key!(key) = self.class.validate_key!(key)
21
+
22
+ def self.parse(raw, root:)
23
+ %w[roles lanes owner].each do |stale_key|
24
+ next unless raw.key?(stale_key)
25
+
26
+ raise BadManifest.new(
27
+ "unknown top-level key '#{stale_key}' — protocol textus/4 fixes roles/lanes in " \
28
+ "code; remove this section from manifest.yaml (see CHANGELOG)",
29
+ )
30
+ end
31
+ raise BadManifest.new("manifest must declare data:") if Array(raw["data"]).empty? && raw["data"] != {}
32
+
33
+ Schema.validate!(raw)
34
+ new(raw: raw, root: root)
35
+ end
36
+
37
+ def initialize(raw:, root:)
38
+ @raw = raw
39
+ @root = root
40
+ @declared_lane_kinds = Textus::Protocol::V4::LANES.keys.to_h { |name| [name, name] }
41
+ @lane_descs = Textus::Protocol::V4::LANES.transform_values { |row| row[:desc] }
42
+ @audit_config = build_audit_config(raw)
43
+ @worker_config = build_worker_config(raw)
44
+ @role_caps = {}
45
+ @entries = build_entries(raw)
46
+ validate_declared_keys!
47
+ freeze
48
+ end
49
+
50
+ private
51
+
52
+ def build_audit_config(raw)
53
+ a = raw["audit"] || {}
54
+ {
55
+ max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
56
+ keep: a["keep"] || AUDIT_DEFAULTS[:keep],
57
+ }.freeze
58
+ end
59
+
60
+ def build_worker_config(raw)
61
+ w = raw["worker"] || {}
62
+ {
63
+ pool: w["pool"] || WORKER_DEFAULTS[:pool],
64
+ poll: w["poll"] || WORKER_DEFAULTS[:poll],
65
+ lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
66
+ max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
67
+ }.freeze
68
+ end
69
+
70
+ def build_entries(raw)
71
+ (raw["data"] || {}).flat_map do |lane_name, lane_entries|
72
+ unless Textus::Protocol::V4::LANES.key?(lane_name)
73
+ raise BadManifest.new("unknown lane '#{lane_name}' (known: #{Textus::Protocol::V4::LANES.keys.join(", ")})")
74
+ end
75
+
76
+ Array(lane_entries).map { |e| Manifest::Entry::Parser.call(e.merge("lane" => lane_name), root: @root) }
77
+ end.freeze
78
+ end
79
+
80
+ def validate_declared_keys!
81
+ @entries.each do |e|
82
+ raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
83
+
84
+ Protocol::Key::Grammar.validate!(e.key)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,95 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Base < Entry
6
+ attr_reader :raw, :key, :path, :lane, :format, :publish_targets, :schema, :naming
7
+
8
+ # rubocop:disable Lint/MissingSuper
9
+ def initialize(raw:, key:, path:, lane:, format:, schema: nil, naming: nil, publish_targets: [])
10
+ @raw = raw
11
+ @key = key
12
+ @path = path
13
+ @lane = lane
14
+ @schema = schema
15
+ @naming = naming
16
+ @format = format
17
+ @publish_targets = Array(publish_targets)
18
+ end
19
+ # rubocop:enable Lint/MissingSuper
20
+
21
+ def lane_writers(policy)
22
+ policy.writers_for(@lane)
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 derived? = false
30
+ def nested? = false
31
+ def leaf? = false
32
+
33
+ # Production traits. Default false on all non-produced entries.
34
+ # Lets publish modes call these without a `respond_to?` guard.
35
+ def external? = false
36
+
37
+ # Whether git should track this entry's file. Default true; an entry
38
+ # marked `tracked: false` in the manifest stays protocol-readable but is
39
+ # listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
40
+ # reads from raw here rather than threading through every constructor.
41
+ def tracked? = @raw["tracked"] != false
42
+
43
+ # Single source of truth is @publish_targets (ADR 0094). These
44
+ # derive the ADR-0049/0052 views the publish modes consume.
45
+ def publish_to = @publish_targets.select(&:to_target?).map(&:to)
46
+ def publish_tree = @publish_targets.find(&:tree_target?)&.tree
47
+
48
+ def events = {}
49
+
50
+ # Minimal context object passed into entry `publish_via` hooks.
51
+ # Everything beyond the three primitives is derived.
52
+ PublishContext = ::Data.define(:container, :reader) do
53
+ def manifest = container.manifest
54
+ def root = container.root
55
+ def repo_root = File.dirname(container.root)
56
+ def store_root = container.root
57
+
58
+ # No-op: event bus removed in workflow redesign; callers that fire
59
+ # :entry_published / :entry_produced remain unchanged in the source.
60
+ def emit(_event, **_payload) = nil
61
+
62
+ # Read a named template from the store's templates/ directory.
63
+ # Raises TemplateError when the file doesn't exist.
64
+ def read_template(name)
65
+ path = container.layout.template_path(name)
66
+ unless File.exist?(path)
67
+ raise Textus::TemplateError.new(
68
+ "template '#{name}' not found",
69
+ template_name: name,
70
+ )
71
+ end
72
+ File.read(path)
73
+ end
74
+ end
75
+
76
+ # ADR 0049: an entry resolves, once, to one Publish::* mode that owns its
77
+ # publish algorithm. A plain entry publishes via ToPaths (publish_to) or
78
+ # None; Nested resolves among the key/path-driven modes. Derived
79
+ # overrides publish_via to materialize first.
80
+ def publish_mode
81
+ @publish_mode ||= Publish.resolve(self)
82
+ end
83
+
84
+ def publish_via(pctx, prefix: nil)
85
+ publish_mode.publish(pctx, prefix: prefix)
86
+ end
87
+
88
+ def with_publish_targets(pts)
89
+ dup.tap { |e| e.instance_variable_set(:@publish_targets, pts) }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Leaf < Base
6
+ KIND = :leaf
7
+
8
+ def leaf? = true
9
+
10
+ def self.from_raw(common, _raw)
11
+ new(**common)
12
+ end
13
+
14
+ Entry::REGISTRY[KIND] = self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ class Nested < Base
6
+ def nested? = true
7
+
8
+ KIND = :nested
9
+
10
+ def self.from_raw(common, _raw)
11
+ new(**common)
12
+ end
13
+
14
+ Entry::REGISTRY[KIND] = self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ module Parser
6
+ def self.call(raw, root: nil)
7
+ key = raw["key"] or raise UsageError.new("manifest entry missing key")
8
+ lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
9
+
10
+ naming = raw["naming"]
11
+ if naming && !Textus::Protocol::V4::NAMING_PATTERNS.key?(naming)
12
+ known_aliases = Textus::Protocol::V4::NAMING_PATTERNS.keys.join(", ")
13
+ raise BadManifest.new(
14
+ "entry '#{key}': unknown naming alias '#{naming}' (known: #{known_aliases})",
15
+ )
16
+ end
17
+
18
+ kind = infer_kind(key, raw, root:)
19
+ format = resolve_format(key, kind, root, raw["format"])
20
+ path = derive_path(key, kind, format)
21
+
22
+ common = {
23
+ raw: raw,
24
+ key: key, path: path, lane: lane,
25
+ format: format, naming: naming,
26
+ schema: raw["schema"]
27
+ }
28
+
29
+ klass = Entry::REGISTRY[kind]
30
+ raise BadManifest.new("entry '#{key}': unknown inferred kind: #{kind.inspect}") unless klass
31
+
32
+ klass.from_raw(common, raw)
33
+ end
34
+
35
+ def self.infer_kind(key, _raw, root:)
36
+ base = key.split(".").join("/")
37
+ dir = root ? File.join(root, "data", base) : nil
38
+
39
+ return :nested if dir && File.directory?(dir)
40
+ return :leaf if dir && Dir.glob("#{dir}.*").any? { |p| File.file?(p) }
41
+
42
+ :leaf
43
+ end
44
+ private_class_method :infer_kind
45
+
46
+ def self.resolve_format(key, kind, root, declared)
47
+ return declared || "markdown" if kind == :nested
48
+
49
+ dir = root ? File.join(root, "data", key.split(".").join("/")) : nil
50
+ if dir
51
+ existing = Dir.glob("#{dir}.*").find { |p| File.file?(p) }
52
+ return Textus::Protocol::Format.infer_from_extension(File.extname(existing)) if existing
53
+ end
54
+
55
+ declared || "markdown"
56
+ end
57
+
58
+ def self.derive_path(key, kind_sym, format)
59
+ dir_path = key.split(".").join("/")
60
+ return dir_path if kind_sym == :nested
61
+
62
+ ext = Textus::Protocol::Format.for(format).extensions.first
63
+ "#{dir_path}#{ext}"
64
+ end
65
+
66
+ private_class_method :derive_path
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,209 @@
1
+ require "tempfile"
2
+
3
+ module Textus
4
+ module Protocol
5
+ class Manifest
6
+ class Entry
7
+ module Publish
8
+ def self.resolve(entry)
9
+ set = []
10
+ set << "publish.to" unless Array(entry.publish_to).empty?
11
+ set << "publish.tree" unless entry.publish_tree.nil?
12
+
13
+ if set.length > 1
14
+ raise Textus::UsageError.new(
15
+ "entry '#{entry.key}': #{set.join(" and ")} are mutually exclusive — an entry publishes exactly one way",
16
+ )
17
+ end
18
+
19
+ mode_for(entry, set.first)
20
+ end
21
+
22
+ def self.mode_for(entry, key)
23
+ case key
24
+ when "publish.to" then ToPaths.new(entry)
25
+ when "publish.tree" then Tree.new(entry)
26
+ else None.new(entry)
27
+ end
28
+ end
29
+ private_class_method :mode_for
30
+
31
+ class Mode
32
+ def initialize(entry)
33
+ @entry = entry
34
+ end
35
+
36
+ attr_reader :entry
37
+
38
+ def validate!; end
39
+
40
+ def keyless? = false
41
+
42
+ private
43
+
44
+ def repo_abs(pctx, rel)
45
+ File.expand_path(File.join(pctx.repo_root, rel))
46
+ end
47
+
48
+ def inside_repo?(pctx, abs)
49
+ abs.start_with?(File.expand_path(pctx.repo_root) + File::SEPARATOR)
50
+ end
51
+
52
+ def store_base(pctx)
53
+ File.join(pctx.root, Textus::Protocol::Key::Path.normalize_relative_path(entry.path))
54
+ end
55
+ end
56
+
57
+ class None < Mode
58
+ def publish(_pctx, prefix: nil) = nil
59
+ end
60
+
61
+ # publish.to: render or copy the entry's stored data to each fixed repo path.
62
+ class ToPaths < Mode
63
+ def initialize(entry, publisher: Textus::Infra::Port::Publisher.new)
64
+ super(entry)
65
+ @publisher = publisher
66
+ end
67
+
68
+ def publish(pctx, prefix: nil)
69
+ targets = entry.publish_targets.select(&:to_target?)
70
+ return nil if targets.empty?
71
+
72
+ data_path = pctx.manifest.resolver.resolve(entry.key).path
73
+ envelope = pctx.reader.call(key: entry.key)
74
+ content = nil
75
+
76
+ targets.each do |t|
77
+ if t.renders?
78
+ content ||= Textus::Protocol::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
79
+ publish_bytes(render_bytes(t, content, pctx), entry.key, t, pctx, data_path, envelope)
80
+ elsif strip_meta?(entry)
81
+ content ||= Textus::Protocol::Format.for(entry.format).parse(File.read(data_path), path: data_path)["content"]
82
+ bytes = Textus::Protocol::Format.for(entry.format).serialize(meta: {}, body: "", content: content)
83
+ publish_bytes(bytes, entry.key, t, pctx, data_path, envelope)
84
+ else
85
+ target_abs = File.join(pctx.repo_root, t.to)
86
+ @publisher.publish(source: data_path, target: target_abs, store_root: pctx.root)
87
+ pctx.emit(:entry_published, key: entry.key, envelope: envelope, source: data_path, target: target_abs)
88
+ end
89
+ end
90
+
91
+ { kind: :built, value: { "key" => entry.key, "path" => data_path, "published_to" => targets.map(&:to) } }
92
+ end
93
+
94
+ private
95
+
96
+ def strip_meta?(entry)
97
+ %w[json yaml].include?(entry.format.to_s)
98
+ end
99
+
100
+ def render_bytes(target, content, pctx)
101
+ boot = target.inject_boot ? Textus::Protocol::Boot.build(container: pctx.container) : nil
102
+ renderer = Textus::Protocol::Produce::Render.new(
103
+ template_loader: ->(n) { pctx.read_template(n) },
104
+ manifest: pctx.manifest,
105
+ source_publish_path: target.to,
106
+ entry_key: entry.key,
107
+ edge_store: pctx.container.store.link_store,
108
+ )
109
+ renderer.bytes_for(target: target, data: content, boot: boot)
110
+ end
111
+
112
+ def publish_bytes(bytes, key, target, pctx, data_path, envelope)
113
+ target_abs = File.join(pctx.repo_root, target.to)
114
+ Tempfile.create(["textus-publish", File.extname(target.to)]) do |f|
115
+ f.binmode
116
+ f.write(bytes)
117
+ f.flush
118
+ @publisher.publish(
119
+ source: f.path, target: target_abs, store_root: pctx.root, provenance_source: data_path,
120
+ )
121
+ end
122
+ pctx.emit(:entry_published, key: key, envelope: envelope, source: data_path, target: target_abs)
123
+ end
124
+ end
125
+
126
+ # publish.tree: mirror this entry's whole subtree to one target dir.
127
+ class Tree < Mode
128
+ def keyless? = true
129
+
130
+ def publish(pctx, prefix: nil)
131
+ target_rel = entry.publish_tree
132
+ target_dir = repo_abs(pctx, target_rel)
133
+ unless inside_repo?(pctx, target_dir)
134
+ raise Textus::PublishError.new(
135
+ "entry '#{entry.key}': publish_tree target '#{target_rel}' escapes repo root",
136
+ )
137
+ end
138
+
139
+ result = SubtreeMirror.new(entry, pctx).mirror(
140
+ walk_root: store_base(pctx),
141
+ target_dir: target_dir,
142
+ key: entry.key,
143
+ envelope: nil,
144
+ )
145
+ { kind: :leaves, value: result[:written], pruned: result[:pruned] }
146
+ end
147
+
148
+ def validate!
149
+ publish_tree = entry.publish_tree
150
+ raise UsageError.new("entry '#{entry.key}': publish.tree must be a string") unless publish_tree.is_a?(String)
151
+
152
+ used_vars = publish_tree.scan(/\{([a-z]+)\}/).flatten
153
+ return if used_vars.empty?
154
+
155
+ raise UsageError.new(
156
+ "entry '#{entry.key}': publish.tree names a single directory and takes no template variable(s) " \
157
+ "#{used_vars.map { |v| "{#{v}}" }.join(", ")} — it mirrors the whole subtree to one target dir.",
158
+ )
159
+ end
160
+ end
161
+
162
+ class SubtreeMirror
163
+ def initialize(entry, pctx, publisher: Textus::Infra::Port::Publisher.new)
164
+ @entry = entry
165
+ @pctx = pctx
166
+ @publisher = publisher
167
+ end
168
+
169
+ def mirror(walk_root:, target_dir:, key:, envelope:)
170
+ return { written: [], pruned: [] } unless File.directory?(walk_root)
171
+
172
+ written = publish_files(walk_root: walk_root, target_dir: target_dir, key: key, envelope: envelope)
173
+ { written: written, pruned: prune(target_dir, written) }
174
+ end
175
+
176
+ private
177
+
178
+ def publish_files(walk_root:, target_dir:, key:, envelope:)
179
+ Dir.glob(File.join(walk_root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |src|
180
+ next nil unless File.file?(src)
181
+
182
+ dst = File.join(target_dir, relative(src, walk_root))
183
+ @publisher.publish(source: src, target: dst, store_root: @pctx.root)
184
+ @pctx.emit(:entry_published, key: key, envelope: envelope, source: src, target: dst)
185
+ { "key" => key, "source" => src, "target" => dst }
186
+ end
187
+ end
188
+
189
+ def prune(target_dir, written)
190
+ kept = written.map { |w| File.expand_path(w["target"]) }
191
+ store = Textus::Infra::Port::SentinelStore.new
192
+ store.targets_under(target_dir, @pctx.root).filter_map do |managed|
193
+ abs = File.expand_path(managed)
194
+ next nil if kept.include?(abs)
195
+
196
+ @publisher.unpublish(target: managed, store_root: @pctx.root)
197
+ managed
198
+ end
199
+ end
200
+
201
+ def relative(path, root)
202
+ path.sub(%r{\A#{Regexp.escape(root)}/}, "")
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,10 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Entry
5
+ # Populated by each Entry::* subclass at load time.
6
+ REGISTRY = {}
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Policy
5
+ # One publish destination (ADR 0094). Exactly one of:
6
+ # to-target { to:, template:?, inject_boot:? } — render data through a
7
+ # template, or copy verbatim when no template
8
+ # tree-target { tree: } — ADR 0052 subtree mirror
9
+ # Provenance is NOT a publish flag — it lives in the data's `_meta`.
10
+ class PublishTarget
11
+ attr_reader :to, :tree, :template, :inject_boot
12
+
13
+ def initialize(raw)
14
+ if raw.key?("provenance")
15
+ raise Textus::BadManifest.new("publish `provenance:` was removed (ADR 0094): provenance lives in the data's `_meta`")
16
+ end
17
+
18
+ @to = raw["to"]
19
+ @tree = raw["tree"]
20
+ raise Textus::BadManifest.new("a publish target needs exactly one of `to:` or `tree:`") unless @to.nil? ^ @tree.nil?
21
+
22
+ @template = raw["template"]
23
+ @inject_boot = raw["inject_boot"] == true
24
+ return unless tree_target? && (@template || @inject_boot)
25
+
26
+ raise Textus::BadManifest.new("a tree target takes no template/inject_boot (ADR 0094)")
27
+ end
28
+
29
+ def to_target? = !@to.nil?
30
+ def tree_target? = !@tree.nil?
31
+ def renders? = to_target? && !@template.nil?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ module Textus
2
+ module Protocol
3
+ class Manifest
4
+ class Policy
5
+ PROPOSALS_WRITERS = %w[human agent].freeze
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ @entries = nil
10
+ end
11
+
12
+ attr_writer :entries
13
+
14
+ # ── Authorization ──
15
+
16
+ def allowed?(role:, lane:)
17
+ writers_for(lane).map(&:to_sym).include?(role.to_sym)
18
+ end
19
+
20
+ def allowed!(role:, lane:, key: nil)
21
+ return true if allowed?(role:, lane:)
22
+
23
+ raise Textus::WriteForbidden.new(key, lane, holders: writers_for(lane))
24
+ end
25
+
26
+ def writers_for(lane_name)
27
+ return PROPOSALS_WRITERS if lane_name == "proposals"
28
+
29
+ lane_map = Textus::Protocol::V4::LANES[lane_name]
30
+ lane_map ? Array(lane_map[:writers]) : []
31
+ end
32
+
33
+ def actor_for(lane_name)
34
+ writers_for(lane_name).first
35
+ end
36
+
37
+ def proposer_role
38
+ proposers = writers_for("proposals")
39
+ authors = writers_for("knowledge")
40
+ (proposers - authors).first || proposers.first
41
+ end
42
+
43
+ # ── Existing methods (unchanged) ──
44
+
45
+ def derived_entry?(key)
46
+ entry = Array(@entries).find { |e| e.key == key }
47
+ entry && entry.lane == "artifacts"
48
+ end
49
+
50
+ def queue_lane
51
+ "scratchpad.proposals"
52
+ end
53
+
54
+ def machine_lane
55
+ "artifacts"
56
+ end
57
+
58
+ def queue_lane?(lane_name)
59
+ lane_name == "scratchpad"
60
+ end
61
+
62
+ def propose_lane_for(role)
63
+ proposers = writers_for("proposals")
64
+ proposers.include?(role) ? "scratchpad.proposals" : nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end