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,29 +0,0 @@
1
- module Textus
2
- class Manifest
3
- # Resolves a manifest's `roles:` block (or the absence of one) into a
4
- # capability map: { role_name => [verbs] }. Verbs are a subset of the
5
- # closed capability set (Schema::CAPABILITIES). See ADR 0030.
6
- module Capabilities
7
- # Fallback role set for a manifest that omits `roles:` entirely. Agent
8
- # is intentionally minimal here (`propose` only) — narrower than the
9
- # `textus init` scaffold, which declares `agent: [propose, keep]` so the
10
- # default `notebook` workspace is writable. A roles-less manifest that
11
- # declares a `kind: workspace` zone is therefore rejected at load (no
12
- # `keep`-holder); declare `roles:` to opt into a workspace lane (ADR 0033).
13
- DEFAULT_MAPPING = {
14
- Textus::Value::Role::HUMAN => %w[author propose].freeze,
15
- Textus::Value::Role::AGENT => %w[propose].freeze,
16
- Textus::Value::Role::AUTOMATION => %w[converge].freeze,
17
- }.freeze
18
-
19
- # Returns { role_name => [verbs] }. When `roles:` is declared we use
20
- # exactly that; defaults are *not* layered in (declaring roles is an
21
- # opt-in to a fully user-defined vocabulary).
22
- def self.resolve(raw_roles)
23
- return DEFAULT_MAPPING if raw_roles.nil?
24
-
25
- raw_roles.to_h { |r| [r["name"], Array(r["can"]).freeze] }.freeze
26
- end
27
- end
28
- end
29
- end
@@ -1,102 +0,0 @@
1
- require_relative "schema"
2
- require_relative "capabilities"
3
-
4
- module Textus
5
- class Manifest
6
- # Immutable, parsed view of a manifest YAML document.
7
- #
8
- # Holds raw structural data (lanes, entries, audit_config, role_caps)
9
- # but no behaviour beyond accessors. Behaviour (lane authority, key
10
- # resolution, rules) lives on Manifest::Policy / Resolver / Rules.
11
- class Data
12
- AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 5 }.freeze
13
- WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
14
-
15
- attr_reader :raw, :root, :entries, :declared_lane_kinds,
16
- :lane_descs, :lane_owners,
17
- :audit_config, :worker_config, :role_caps, :policy
18
-
19
- def self.validate_key!(key)
20
- raise UsageError.new("empty key") if key.nil? || key.empty?
21
-
22
- Key::Grammar.validate!(key)
23
- end
24
-
25
- # Forwarder used by Resolver and Entry classes that received a Data
26
- # but were written against the historical Manifest API.
27
- def validate_key!(key) = self.class.validate_key!(key)
28
-
29
- def self.parse(raw, root:)
30
- raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
31
-
32
- Schema.validate!(raw)
33
- new(raw: raw, root: root)
34
- end
35
-
36
- def initialize(raw:, root:)
37
- @raw = raw
38
- @root = root
39
- # Write authority is derived from capabilities × lane-kind (ADR 0030),
40
- # not a per-lane writer list. "Which lanes are declared" lives in the
41
- # one kind-keyed map below (declared_lane_kinds); membership checks by
42
- # read-side callers (boot, maintenance/data_mv) use its keyset (ADR 0034).
43
- @declared_lane_kinds = Array(raw["lanes"]).to_h do |z|
44
- [z["name"], z["kind"]&.to_sym]
45
- end
46
- @lane_descs = Array(raw["lanes"]).to_h { |z| [z["name"], z["desc"]] }
47
- # Only lanes that actually declare an owner — keep nil-tombstones out so a
48
- # future `lane_owners.key?(name)` means "owner declared", not "lane exists".
49
- @lane_owners = Array(raw["lanes"]).to_h { |z| [z["name"], z["owner"]] }.compact
50
- @audit_config = build_audit_config(raw)
51
- @worker_config = build_worker_config(raw)
52
- @role_caps = Capabilities.resolve(raw["roles"])
53
- # Policy is constructed before entries because Entry validators
54
- # use the entry's own `derived?` and similar helpers that call into
55
- # Policy; Policy must exist before entries are built.
56
- @policy = Policy.new(self)
57
- @entries = build_entries(raw)
58
- validate_declared_keys!
59
- freeze
60
- end
61
-
62
- private
63
-
64
- def build_audit_config(raw)
65
- a = raw["audit"] || {}
66
- {
67
- max_size: a["max_size"] || AUDIT_DEFAULTS[:max_size],
68
- keep: a["keep"] || AUDIT_DEFAULTS[:keep],
69
- }.freeze
70
- end
71
-
72
- # Worker/queue tunables (ADR: job-queue execution model). All optional;
73
- # the daemon (serve) and batch drain read these, falling back to defaults
74
- # so a manifest with no `worker:` block runs the queue out of the box.
75
- def build_worker_config(raw)
76
- w = raw["worker"] || {}
77
- {
78
- pool: w["pool"] || WORKER_DEFAULTS[:pool],
79
- poll: w["poll"] || WORKER_DEFAULTS[:poll],
80
- lease_ttl: w["lease_ttl"] || WORKER_DEFAULTS[:lease_ttl],
81
- max_attempts: w["max_attempts"] || WORKER_DEFAULTS[:max_attempts],
82
- }.freeze
83
- end
84
-
85
- def build_entries(raw)
86
- Array(raw["entries"]).map do |e|
87
- entry = Manifest::Entry::Parser.call(e)
88
- Manifest::Entry::Validators.run_all(entry, policy: @policy)
89
- entry
90
- end.freeze
91
- end
92
-
93
- def validate_declared_keys!
94
- @entries.each do |e|
95
- raise UsageError.new("empty key") if e.key.nil? || e.key.empty?
96
-
97
- Key::Grammar.validate!(e.key)
98
- end
99
- end
100
- end
101
- end
102
- end
@@ -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 Metrics/ParameterLists, 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 Metrics/ParameterLists, 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.geometry.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