textus 0.51.0 → 0.53.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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +74 -66
  4. data/SPEC.md +100 -223
  5. data/docs/architecture/README.md +91 -80
  6. data/docs/reference/conventions.md +8 -8
  7. data/exe/textus +1 -1
  8. data/lib/textus/action/accept.rb +53 -0
  9. data/lib/textus/action/audit.rb +133 -0
  10. data/lib/textus/action/base.rb +42 -0
  11. data/lib/textus/{read → action}/blame.rb +30 -22
  12. data/lib/textus/action/boot.rb +26 -0
  13. data/lib/textus/action/data_mv.rb +71 -0
  14. data/lib/textus/action/deps.rb +48 -0
  15. data/lib/textus/action/doctor.rb +26 -0
  16. data/lib/textus/action/drain.rb +41 -0
  17. data/lib/textus/action/enqueue.rb +55 -0
  18. data/lib/textus/action/get.rb +80 -0
  19. data/lib/textus/action/jobs.rb +38 -0
  20. data/lib/textus/action/key_delete.rb +46 -0
  21. data/lib/textus/action/key_delete_prefix.rb +46 -0
  22. data/lib/textus/action/key_mv.rb +143 -0
  23. data/lib/textus/action/key_mv_prefix.rb +59 -0
  24. data/lib/textus/action/list.rb +44 -0
  25. data/lib/textus/action/propose.rb +54 -0
  26. data/lib/textus/action/published.rb +26 -0
  27. data/lib/textus/action/pulse/scanner.rb +118 -0
  28. data/lib/textus/action/pulse.rb +87 -0
  29. data/lib/textus/action/put.rb +63 -0
  30. data/lib/textus/action/rdeps.rb +49 -0
  31. data/lib/textus/action/reject.rb +49 -0
  32. data/lib/textus/action/rule_explain.rb +95 -0
  33. data/lib/textus/action/rule_lint.rb +70 -0
  34. data/lib/textus/action/rule_list.rb +46 -0
  35. data/lib/textus/action/schema_envelope.rb +31 -0
  36. data/lib/textus/action/uid.rb +35 -0
  37. data/lib/textus/action/where.rb +38 -0
  38. data/lib/textus/action/write_verb.rb +58 -0
  39. data/lib/textus/background/job/base.rb +27 -0
  40. data/lib/textus/background/job/materialize.rb +31 -0
  41. data/lib/textus/background/job/refresh.rb +22 -0
  42. data/lib/textus/background/job/sweep.rb +31 -0
  43. data/lib/textus/background/job.rb +19 -0
  44. data/lib/textus/background/plan.rb +9 -0
  45. data/lib/textus/background/planner/plan.rb +113 -0
  46. data/lib/textus/background/retention/apply.rb +50 -0
  47. data/lib/textus/background/worker.rb +67 -0
  48. data/lib/textus/boot.rb +59 -49
  49. data/lib/textus/command.rb +36 -0
  50. data/lib/textus/container.rb +1 -1
  51. data/lib/textus/{domain → core}/duration.rb +1 -1
  52. data/lib/textus/{domain → core}/freshness/evaluator.rb +13 -13
  53. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  54. data/lib/textus/{domain → core}/freshness.rb +2 -2
  55. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  56. data/lib/textus/{domain → core}/retention.rb +2 -2
  57. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  58. data/lib/textus/doctor/check/generator_drift.rb +2 -2
  59. data/lib/textus/doctor/check/handler_permit.rb +34 -0
  60. data/lib/textus/doctor/check/hooks.rb +11 -18
  61. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  62. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  63. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  64. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  65. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  66. data/lib/textus/doctor/check/sentinels.rb +2 -2
  67. data/lib/textus/doctor/check.rb +12 -9
  68. data/lib/textus/{read → doctor}/validator.rb +22 -13
  69. data/lib/textus/doctor.rb +6 -6
  70. data/lib/textus/envelope/io/writer.rb +65 -36
  71. data/lib/textus/envelope.rb +5 -3
  72. data/lib/textus/errors.rb +19 -11
  73. data/lib/textus/events.rb +21 -0
  74. data/lib/textus/gate/auth.rb +181 -0
  75. data/lib/textus/gate.rb +114 -0
  76. data/lib/textus/init/templates/machine_intake.rb +40 -36
  77. data/lib/textus/init/templates/orientation_reducer.rb +15 -11
  78. data/lib/textus/init.rb +93 -76
  79. data/lib/textus/key/path.rb +9 -2
  80. data/lib/textus/layout.rb +21 -0
  81. data/lib/textus/manifest/capabilities.rb +1 -1
  82. data/lib/textus/manifest/data.rb +30 -15
  83. data/lib/textus/manifest/entry/base.rb +15 -11
  84. data/lib/textus/manifest/entry/parser.rb +6 -6
  85. data/lib/textus/manifest/entry/produced.rb +3 -2
  86. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  87. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
  88. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  89. data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
  90. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  91. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  92. data/lib/textus/manifest/policy/react.rb +30 -0
  93. data/lib/textus/{domain → manifest}/policy/retention.rb +4 -4
  94. data/lib/textus/{domain → manifest}/policy/source.rb +27 -28
  95. data/lib/textus/manifest/policy.rb +36 -48
  96. data/lib/textus/manifest/resolver.rb +3 -2
  97. data/lib/textus/manifest/rules.rb +4 -4
  98. data/lib/textus/manifest/schema/keys.rb +18 -12
  99. data/lib/textus/manifest/schema/validator.rb +27 -25
  100. data/lib/textus/manifest/schema/vocabulary.rb +3 -3
  101. data/lib/textus/manifest/schema.rb +2 -2
  102. data/lib/textus/manifest.rb +2 -2
  103. data/lib/textus/{produce → pipeline}/acquire/handler.rb +3 -3
  104. data/lib/textus/{produce → pipeline}/acquire/intake.rb +23 -21
  105. data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
  106. data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
  107. data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
  108. data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
  109. data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
  110. data/lib/textus/{produce → pipeline}/engine.rb +17 -63
  111. data/lib/textus/{produce → pipeline}/render.rb +3 -1
  112. data/lib/textus/ports/audit_log.rb +31 -5
  113. data/lib/textus/ports/audit_subscriber.rb +4 -4
  114. data/lib/textus/ports/build_lock.rb +1 -1
  115. data/lib/textus/ports/queue/job.rb +65 -0
  116. data/lib/textus/ports/queue.rb +130 -0
  117. data/lib/textus/ports/sentinel_store.rb +2 -2
  118. data/lib/textus/ports/watcher_lock.rb +48 -0
  119. data/lib/textus/projection.rb +8 -8
  120. data/lib/textus/role.rb +1 -1
  121. data/lib/textus/schema/tools.rb +4 -3
  122. data/lib/textus/session.rb +6 -3
  123. data/lib/textus/step/base.rb +35 -0
  124. data/lib/textus/step/builtin/csv_fetch.rb +19 -0
  125. data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
  126. data/lib/textus/step/builtin/json_fetch.rb +18 -0
  127. data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
  128. data/lib/textus/step/builtin/rss_fetch.rb +26 -0
  129. data/lib/textus/step/builtin.rb +22 -0
  130. data/lib/textus/{hooks → step}/catalog.rb +3 -4
  131. data/lib/textus/{hooks → step}/context.rb +15 -13
  132. data/lib/textus/step/discovery.rb +24 -0
  133. data/lib/textus/{hooks → step}/error_log.rb +1 -1
  134. data/lib/textus/{hooks → step}/event_bus.rb +15 -16
  135. data/lib/textus/step/fetch.rb +13 -0
  136. data/lib/textus/{hooks → step}/fire_report.rb +1 -1
  137. data/lib/textus/step/loader.rb +108 -0
  138. data/lib/textus/step/observe.rb +31 -0
  139. data/lib/textus/step/registry_store.rb +66 -0
  140. data/lib/textus/{hooks → step}/signature.rb +1 -1
  141. data/lib/textus/step/transform.rb +12 -0
  142. data/lib/textus/step/validate.rb +11 -0
  143. data/lib/textus/step.rb +10 -0
  144. data/lib/textus/store.rb +17 -15
  145. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  146. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  147. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  148. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  149. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  150. data/lib/textus/surfaces/cli/group.rb +50 -0
  151. data/lib/textus/surfaces/cli/runner.rb +236 -0
  152. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  153. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  154. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  155. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  156. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  157. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  158. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  159. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  160. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  161. data/lib/textus/surfaces/cli/verb.rb +111 -0
  162. data/lib/textus/surfaces/cli.rb +148 -0
  163. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  164. data/lib/textus/surfaces/mcp/errors.rb +34 -0
  165. data/lib/textus/surfaces/mcp/server.rb +145 -0
  166. data/lib/textus/surfaces/mcp/session.rb +9 -0
  167. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  168. data/lib/textus/surfaces/mcp.rb +8 -0
  169. data/lib/textus/surfaces/role_scope.rb +38 -0
  170. data/lib/textus/surfaces/watcher.rb +38 -0
  171. data/lib/textus/version.rb +1 -1
  172. data/lib/textus.rb +64 -22
  173. metadata +133 -107
  174. data/lib/textus/cli/group/hook.rb +0 -9
  175. data/lib/textus/cli/group/key.rb +0 -9
  176. data/lib/textus/cli/group/mcp.rb +0 -9
  177. data/lib/textus/cli/group/rule.rb +0 -9
  178. data/lib/textus/cli/group/schema.rb +0 -9
  179. data/lib/textus/cli/group/zone.rb +0 -9
  180. data/lib/textus/cli/group.rb +0 -48
  181. data/lib/textus/cli/runner.rb +0 -193
  182. data/lib/textus/cli/verb/doctor.rb +0 -17
  183. data/lib/textus/cli/verb/get.rb +0 -18
  184. data/lib/textus/cli/verb/hook_run.rb +0 -48
  185. data/lib/textus/cli/verb/hooks.rb +0 -50
  186. data/lib/textus/cli/verb/init.rb +0 -18
  187. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  188. data/lib/textus/cli/verb/put.rb +0 -30
  189. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  190. data/lib/textus/cli/verb/schema_init.rb +0 -19
  191. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  192. data/lib/textus/cli/verb.rb +0 -116
  193. data/lib/textus/cli.rb +0 -138
  194. data/lib/textus/dispatcher.rb +0 -52
  195. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  196. data/lib/textus/domain/action.rb +0 -9
  197. data/lib/textus/domain/permission.rb +0 -7
  198. data/lib/textus/domain/policy/base_guards.rb +0 -25
  199. data/lib/textus/domain/policy/evaluation.rb +0 -15
  200. data/lib/textus/domain/policy/guard.rb +0 -35
  201. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  202. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  203. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  204. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  205. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  206. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  207. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  208. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  209. data/lib/textus/hooks/builtin.rb +0 -70
  210. data/lib/textus/hooks/loader.rb +0 -54
  211. data/lib/textus/hooks/rpc_registry.rb +0 -43
  212. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  213. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  214. data/lib/textus/maintenance/reconcile.rb +0 -160
  215. data/lib/textus/maintenance/rule_lint.rb +0 -66
  216. data/lib/textus/maintenance/zone_mv.rb +0 -64
  217. data/lib/textus/maintenance.rb +0 -15
  218. data/lib/textus/mcp/catalog.rb +0 -70
  219. data/lib/textus/mcp/errors.rb +0 -32
  220. data/lib/textus/mcp/server.rb +0 -138
  221. data/lib/textus/mcp/session.rb +0 -7
  222. data/lib/textus/mcp/tool_schemas.rb +0 -15
  223. data/lib/textus/mcp.rb +0 -6
  224. data/lib/textus/mustache.rb +0 -117
  225. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -69
  226. data/lib/textus/produce/events.rb +0 -36
  227. data/lib/textus/read/audit.rb +0 -130
  228. data/lib/textus/read/boot.rb +0 -26
  229. data/lib/textus/read/capabilities.rb +0 -70
  230. data/lib/textus/read/deps.rb +0 -38
  231. data/lib/textus/read/doctor.rb +0 -27
  232. data/lib/textus/read/freshness.rb +0 -152
  233. data/lib/textus/read/get.rb +0 -73
  234. data/lib/textus/read/list.rb +0 -24
  235. data/lib/textus/read/published.rb +0 -22
  236. data/lib/textus/read/pulse.rb +0 -98
  237. data/lib/textus/read/rdeps.rb +0 -39
  238. data/lib/textus/read/rule_explain.rb +0 -96
  239. data/lib/textus/read/rule_list.rb +0 -54
  240. data/lib/textus/read/schema_envelope.rb +0 -25
  241. data/lib/textus/read/uid.rb +0 -29
  242. data/lib/textus/read/validate_all.rb +0 -36
  243. data/lib/textus/read/where.rb +0 -24
  244. data/lib/textus/role_scope.rb +0 -78
  245. data/lib/textus/write/accept.rb +0 -58
  246. data/lib/textus/write/key_delete.rb +0 -65
  247. data/lib/textus/write/key_mv.rb +0 -141
  248. data/lib/textus/write/propose.rb +0 -54
  249. data/lib/textus/write/put.rb +0 -74
  250. data/lib/textus/write/reject.rb +0 -68
@@ -1,9 +1,9 @@
1
1
  module Textus
2
- module Domain
3
- module Policy
2
+ class Manifest
3
+ class Policy
4
4
  # Garbage collection (ADR 0093). A glob-matched rule slot: when an entry
5
5
  # ages past `ttl`, retire it. Destructive only — runs on the full
6
- # `reconcile` pass, never on a write (ADR 0079's invariant). Orthogonal to
6
+ # `converge` pass, never on a write (ADR 0079's invariant). Orthogonal to
7
7
  # production (`source:`): an intake entry can re-pull hourly AND archive
8
8
  # after 90 days. `warn`/`refresh` are gone (refresh is implied by an
9
9
  # intake source; warn never fired after ADR 0089's pure-read get).
@@ -21,7 +21,7 @@ module Textus
21
21
  raise Textus::BadManifest.new("retention action must be one of #{ACTIONS.join("|")}, got #{raw["action"].inspect}")
22
22
  end
23
23
 
24
- def ttl_seconds = Textus::Domain::Duration.seconds(@ttl)
24
+ def ttl_seconds = Textus::Core::Duration.seconds(@ttl)
25
25
  def destructive? = true
26
26
  end
27
27
  end
@@ -1,17 +1,17 @@
1
1
  module Textus
2
- module Domain
3
- module Policy
2
+ class Manifest
3
+ class Policy
4
4
  # An entry's data-acquisition declaration (ADR 0094). `source:` says HOW the
5
5
  # entry's data is acquired; rendering is a publish concern, so there are no
6
6
  # template/render fields here. `from` is the acquire + staleness axis:
7
- # from: project -> derived (internal projection; observable -> rdeps staleness)
8
- # from: handler -> intake (external fetch; unobservable -> ttl staleness)
9
- # from: command -> external (out-of-band runner; staleness only, textus never runs it)
10
- # `on_write` (sync|async, default async) is the write-trigger strategy for
11
- # observable (project) sources; meaningless for intake/command.
7
+ # from: derive -> derived (internal projection; observable -> rdeps staleness)
8
+ # from: fetch -> intake (external fetch; unobservable -> ttl staleness)
9
+ # from: external -> external (out-of-band runner; staleness only, textus never runs it)
10
+ # Materialization is async-only (job-queue model): a write enqueues a
11
+ # `materialize` job, converged by a worker. There is no per-entry write
12
+ # trigger knob.
12
13
  class Source
13
- FROMS = %w[project handler command].freeze
14
- STRATEGIES = %w[async sync].freeze
14
+ FROMS = %w[fetch derive external].freeze
15
15
 
16
16
  attr_reader :from, :handler, :config, :command, :sources
17
17
 
@@ -21,26 +21,25 @@ module Textus
21
21
  raise Textus::BadManifest.new("source.from must be one of #{FROMS.join("|")}, got #{raw["from"].inspect}")
22
22
  end
23
23
 
24
- @on_write = (raw["on_write"] || "async").to_s
25
- unless STRATEGIES.include?(@on_write)
26
- raise Textus::BadManifest.new("source.on_write must be one of #{STRATEGIES.join("/")}, got #{@on_write.inspect}")
27
- end
28
-
29
24
  @ttl = raw["ttl"]
30
25
  @projection = {}
31
26
 
32
27
  case @from
33
- when "project" then init_project(raw)
34
- when "handler" then init_handler(raw)
35
- when "command" then init_command(raw)
28
+ when "fetch" then init_fetch(raw)
29
+ when "derive" then init_derive(raw)
30
+ when "external" then init_external(raw)
36
31
  end
37
32
  end
38
33
 
39
- def kind = @from == "handler" ? :intake : :derived
40
- def external? = @from == "command"
41
- def projection? = @from == "project"
42
- def sync? = @on_write == "sync"
43
- def ttl_seconds = @ttl.nil? ? nil : Textus::Domain::Duration.seconds(@ttl)
34
+ def kind
35
+ { "fetch" => :intake, "derive" => :derived, "external" => :external }.fetch(@from)
36
+ end
37
+
38
+ def fetch? = @from == "fetch"
39
+ def derive? = @from == "derive"
40
+ def external? = @from == "external"
41
+ def projection? = derive?
42
+ def ttl_seconds = @ttl.nil? ? nil : Textus::Core::Duration.seconds(@ttl)
44
43
 
45
44
  # Flattened projection accessors (ADR 0094) — read directly off the source
46
45
  # block; nil when absent or not a projection source.
@@ -55,22 +54,22 @@ module Textus
55
54
 
56
55
  private
57
56
 
58
- def init_project(raw)
57
+ def init_derive(raw)
59
58
  %w[select pluck sort_by transform].each { |f| @projection[f] = raw[f] if raw.key?(f) }
60
59
  return unless @projection["select"].nil? && @projection["transform"].nil?
61
60
 
62
- raise Textus::BadManifest.new("source (from: project) requires `select:` and/or `transform:`")
61
+ raise Textus::BadManifest.new("source (from: derive) requires `select:` and/or `transform:`")
63
62
  end
64
63
 
65
- def init_handler(raw)
64
+ def init_fetch(raw)
66
65
  @handler = raw["handler"] or
67
- raise Textus::BadManifest.new("source (from: handler) requires a `handler:` field")
66
+ raise Textus::BadManifest.new("source (from: fetch) requires a `handler:` field")
68
67
  @config = raw["config"] || {}
69
68
  end
70
69
 
71
- def init_command(raw)
70
+ def init_external(raw)
72
71
  @command = raw["command"] or
73
- raise Textus::BadManifest.new("source (from: command) requires a `command:` field")
72
+ raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
74
73
  @sources = raw["sources"] || []
75
74
  end
76
75
  end
@@ -1,23 +1,23 @@
1
1
  module Textus
2
2
  class Manifest
3
- # Authority over zones and roles derived from a Manifest::Data snapshot.
3
+ # Authority over lanes and roles derived from a Manifest::Data snapshot.
4
4
  # Encapsulates the lookups previously living on Manifest itself
5
- # (zone_writers, permission_for). Write authority is derived from
6
- # capabilities × zone-kind (ADR 0030): each zone-kind requires one verb
7
- # (Schema::KIND_REQUIRES_VERB) and a role may write a zone iff its caps
8
- # include that verb (verb_for_zone, roles_with_capability). Derived /
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
9
  # proposal-queue status is authoritative via the declared-kind family
10
- # (declared_kind, derived_entry?, queue_zone?, queue_zone).
10
+ # (declared_kind, derived_entry?, queue_lane?, queue_lane).
11
11
  class Policy
12
12
  def initialize(data)
13
13
  @data = data
14
14
  end
15
15
 
16
- # The capability a zone's kind requires to be written, or nil if the
17
- # zone declares no kind. declared_kind returns a Symbol; the table is
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
18
  # keyed by String.
19
- def verb_for_zone(zone_name)
20
- kind = declared_kind(zone_name)
19
+ def verb_for_lane(lane_name)
20
+ kind = declared_kind(lane_name)
21
21
  kind && Schema::KIND_REQUIRES_VERB[kind.to_s]
22
22
  end
23
23
 
@@ -41,39 +41,24 @@ module Textus
41
41
  roles_with_capability(verb).first
42
42
  end
43
43
 
44
- # The roles authorized to write `zone_name`: those holding the verb its
45
- # kind requires. Raises on an undeclared zone.
46
- def zone_writers(zone_name)
47
- raise UsageError.new("undeclared zone '#{zone_name}'") unless @data.declared_zone_kinds.key?(zone_name)
48
-
49
- roles_with_capability(verb_for_zone(zone_name))
50
- end
51
-
52
- def permission_for(zone_name)
53
- Textus::Domain::Permission.new(
54
- zone: zone_name,
55
- writers: zone_writers(zone_name),
56
- )
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]
57
47
  end
58
48
 
59
- # The kind declared on a zone in the manifest, or nil if undeclared.
60
- def declared_kind(zone_name)
61
- @data.declared_zone_kinds[zone_name]
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
62
53
  end
63
54
 
64
- # Zone names declaring `kind` (a Symbol), in manifest order. Lets callers
65
- # (boot) name a kind's live zone instance(s) instead of hardcoding names.
66
- def zones_of_kind(kind)
67
- @data.declared_zone_kinds.select { |_name, k| k == kind }.keys
55
+ # The single lane declaring `kind: queue`, or nil. Schema guarantees <=1.
56
+ def queue_lane
57
+ @data.declared_lane_kinds.key(:queue)
68
58
  end
69
59
 
70
- # The single zone declaring `kind: queue`, or nil. Schema guarantees <=1.
71
- def queue_zone
72
- @data.declared_zone_kinds.key(:queue)
73
- end
74
-
75
- # ADR 0091: derived-ness is a property of the ENTRY, not its zone (one
76
- # machine zone holds both intake and derived entries). Resolve the entry
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
77
62
  # and ask it directly. Returns false if entries are not yet built
78
63
  # (validator phase during Data#initialize) — validators must not rely on
79
64
  # cross-entry state during construction.
@@ -84,24 +69,27 @@ module Textus
84
69
  entry.derived?
85
70
  end
86
71
 
87
- # The single zone declaring kind: machine, or nil.
88
- def machine_zone
89
- @data.declared_zone_kinds.key(:machine)
72
+ # The single lane declaring kind: machine, or nil.
73
+ def machine_lane
74
+ @data.declared_lane_kinds.key(:machine)
90
75
  end
91
76
 
92
- # A zone is a proposal queue iff it declares kind: queue.
93
- def queue_zone?(zone_name)
94
- declared_kind(zone_name) == :queue
77
+ # A lane is a proposal queue iff it declares kind: queue.
78
+ def queue_lane?(lane_name)
79
+ declared_kind(lane_name) == :queue
95
80
  end
96
81
 
97
- # The zone a proposer role writes proposals into: the single zone that
82
+ # The lane a proposer role writes proposals into: the single lane that
98
83
  # declares kind: queue, when the role can write it. Returns nil if there
99
- # is no queue zone or the role cannot write it.
100
- def propose_zone_for(role)
84
+ # is no queue lane or the role cannot write it.
85
+ def propose_lane_for(role)
101
86
  return nil if role.nil?
102
87
 
103
- q = queue_zone
104
- return nil unless q && zone_writers(q).include?(role)
88
+ q = queue_lane
89
+ return nil unless q
90
+
91
+ q_verb = verb_for_lane(q)
92
+ return nil unless roles_with_capability(q_verb).include?(role)
105
93
 
106
94
  q
107
95
  end
@@ -54,7 +54,8 @@ module Textus
54
54
  raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless nested_entry?(entry)
55
55
 
56
56
  primary_ext = Textus::Entry.for_format(entry.format).extensions.first
57
- path = File.join(@data.root, "zones", entry.path, *remaining) + primary_ext
57
+ base = Textus::Key::Path.normalize_relative_path(entry.path)
58
+ path = File.join(@data.root, base, *remaining) + primary_ext
58
59
  Resolution.new(entry: entry, path: path, remaining: remaining)
59
60
  end
60
61
  end
@@ -73,7 +74,7 @@ module Textus
73
74
  # addressable store keys in the public `list` surface.
74
75
  return [] if entry.publish_mode.keyless? && !include_keyless
75
76
 
76
- base = File.join(@data.root, "zones", entry.path)
77
+ base = File.join(@data.root, Textus::Key::Path.normalize_relative_path(entry.path))
77
78
  return [] unless File.directory?(base)
78
79
 
79
80
  Dir.glob(File.join(base, nested_glob(entry.format)))
@@ -22,7 +22,7 @@ module Textus
22
22
  def for(key)
23
23
  slots = PICK_FIELDS.to_h { |f| [f, []] }
24
24
  @blocks.each do |b|
25
- next unless Textus::Domain::Policy::Matcher.matches?(b.match, key)
25
+ next unless Textus::Manifest::Policy::Matcher.matches?(b.match, key)
26
26
 
27
27
  slots.each_key { |slot| slots[slot] << b if b.public_send(slot) }
28
28
  end
@@ -30,7 +30,7 @@ module Textus
30
30
  end
31
31
 
32
32
  def explain(key)
33
- @blocks.select { |b| Textus::Domain::Policy::Matcher.matches?(b.match, key) }
33
+ @blocks.select { |b| Textus::Manifest::Policy::Matcher.matches?(b.match, key) }
34
34
  end
35
35
 
36
36
  private
@@ -39,7 +39,7 @@ module Textus
39
39
  return nil if blocks.empty?
40
40
 
41
41
  globs = blocks.map(&:match)
42
- winning = Textus::Domain::Policy::Matcher.pick_most_specific(globs, key: key)
42
+ winning = Textus::Manifest::Policy::Matcher.pick_most_specific(globs, key: key)
43
43
  blocks.find { |b| b.match == winning }&.public_send(slot)
44
44
  end
45
45
 
@@ -57,7 +57,7 @@ module Textus
57
57
 
58
58
  # One dispatch over the registry, replacing the four bespoke parse_*
59
59
  # methods. :deferred carries the raw Hash after a shape check (its
60
- # contents validate later — guard predicates at GuardFactory build time,
60
+ # contents validate later — guard predicates at Dispatch::Auth check time,
61
61
  # ADR 0031); :immediate instantiates the policy class now. :tagged passes
62
62
  # the raw Hash straight to a policy class that is a tagged union and
63
63
  # dispatches on its discriminator field (e.g. upkeep's on:). A mapping
@@ -4,11 +4,11 @@ module Textus
4
4
  # The manifest's key whitelists and the rule-field registry — the schema's
5
5
  # data tables (ADR 0109; the vocabulary lives in Schema::Vocabulary).
6
6
  module Keys
7
- ROOT_KEYS = %w[version roles zones entries rules audit].freeze
7
+ ROOT_KEYS = %w[version roles lanes entries rules audit].freeze
8
8
  ROLE_KEYS = %w[name can].freeze
9
- ZONE_KEYS = %w[name kind owner desc].freeze
9
+ LANE_KEYS = %w[name kind owner desc].freeze
10
10
  ENTRY_KEYS = %w[
11
- key path zone kind schema owner nested format
11
+ key path lane kind schema owner nested format
12
12
  source publish
13
13
  events ignore tracked
14
14
  ].freeze
@@ -23,7 +23,7 @@ module Textus
23
23
  # `inject_boot`/`provenance` fields are kept here so the schema walk can
24
24
  # still emit the migration hint rather than a bare "unknown key".
25
25
  SOURCE_KEYS = %w[
26
- from handler config template project command sources ttl on_write inject_boot provenance
26
+ from handler config template project command sources ttl inject_boot provenance
27
27
  select pluck sort_by transform
28
28
  ].freeze
29
29
  # ADR 0093: rule-level GC slot. drop/archive only (refresh gone).
@@ -40,12 +40,11 @@ module Textus
40
40
  # (in_rule_list / in_rule_explain)
41
41
  #
42
42
  # Per field:
43
- # yaml_key manifest key (handler_allowlist's intake_ prefix
44
- # disambiguates from entry-level intake:, ADR 0059)
45
- # policy_class the Domain::Policy backing the field (nil = raw value)
43
+ # yaml_key manifest key used in a rule block
44
+ # policy_class the Manifest::Policy backing the field (nil = raw value)
46
45
  # validation :immediate (instantiate the policy at parse, surfacing
47
46
  # shape errors eagerly), :deferred (shape-check + carry
48
- # the raw Hash; guard predicates validate at GuardFactory
47
+ # the raw Hash; guard predicates validate at Dispatch::Auth
49
48
  # build time, ADR 0031), or :tagged (pass the raw Hash to a
50
49
  # tagged-union policy that dispatches on its discriminator
51
50
  # field, e.g. upkeep's on:)
@@ -61,9 +60,9 @@ module Textus
61
60
  # Key order here fixes the order of RULE_KEYS (after match), the slots,
62
61
  # the RuleSet members, and the doctor SLOTS.
63
62
  FIELD_REGISTRY = {
64
- handler_allowlist: {
65
- yaml_key: "intake_handler_allowlist",
66
- policy_class: Textus::Domain::Policy::HandlerAllowlist,
63
+ handler_permit: {
64
+ yaml_key: "handler_permit",
65
+ policy_class: Textus::Manifest::Policy::HandlerPermit,
67
66
  validation: :immediate, sub_keys: nil, arg_key: :handlers,
68
67
  in_pick: true, in_ambiguity: true,
69
68
  in_rule_list: true, in_rule_explain: %i[detail]
@@ -77,11 +76,18 @@ module Textus
77
76
  },
78
77
  retention: {
79
78
  yaml_key: "retention",
80
- policy_class: Textus::Domain::Policy::Retention,
79
+ policy_class: Textus::Manifest::Policy::Retention,
81
80
  validation: :tagged, sub_keys: RETENTION_KEYS, arg_key: nil,
82
81
  in_pick: true, in_ambiguity: true,
83
82
  in_rule_list: true, in_rule_explain: %i[lean detail]
84
83
  },
84
+ react: {
85
+ yaml_key: "react",
86
+ policy_class: Textus::Manifest::Policy::React,
87
+ validation: :immediate, sub_keys: nil, arg_key: :raw,
88
+ in_pick: true, in_ambiguity: true,
89
+ in_rule_list: true, in_rule_explain: %i[lean detail]
90
+ },
85
91
  }.freeze
86
92
 
87
93
  RULE_KEYS = (["match"] + FIELD_REGISTRY.values.map { |m| m[:yaml_key] }).freeze
@@ -14,34 +14,36 @@ module Textus
14
14
  raise BadManifest.new("manifest must be a hash") unless raw.is_a?(Hash)
15
15
 
16
16
  walk(raw, ROOT_KEYS, "$")
17
+ raise BadManifest.new("manifest must declare lanes:") if Array(raw["lanes"]).empty?
18
+
17
19
  validate_roles!(raw["roles"])
18
- validate_zones!(raw["zones"])
20
+ validate_lanes!(raw["lanes"])
19
21
  validate_entries!(raw["entries"])
20
- validate_owners!(raw["zones"], raw["entries"])
22
+ validate_owners!(raw["lanes"], raw["entries"])
21
23
  validate_rules!(raw["rules"])
22
24
  walk(raw["audit"], AUDIT_KEYS, "$.audit") if raw["audit"].is_a?(Hash)
23
25
  validate_single_queue!(raw)
24
26
  validate_single_machine!(raw)
25
- validate_zone_kind_consistency!(raw)
27
+ validate_lane_kind_consistency!(raw)
26
28
  end
27
29
 
28
- def validate_zones!(zones)
29
- Array(zones).each_with_index do |z, i|
30
- walk(z, ZONE_KEYS, "$.zones[#{i}]")
30
+ def validate_lanes!(lanes)
31
+ Array(lanes).each_with_index do |z, i|
32
+ walk(z, LANE_KEYS, "$.lanes[#{i}]")
31
33
  if z["kind"].nil?
32
- raise BadManifest.new("zone '#{z["name"]}' at '$.zones[#{i}]' must declare a kind (one of: #{ZONE_KINDS.join(", ")})")
34
+ raise BadManifest.new("lane '#{z["name"]}' at '$.lanes[#{i}]' must declare a kind (one of: #{LANE_KINDS.join(", ")})")
33
35
  end
34
- next if ZONE_KINDS.include?(z["kind"])
36
+ next if LANE_KINDS.include?(z["kind"])
35
37
 
36
38
  if %w[quarantine derived].include?(z["kind"])
37
39
  raise BadManifest.new(
38
- "zone kind '#{z["kind"]}' at '$.zones[#{i}]' was folded into 'machine' (ADR 0091) — " \
40
+ "lane kind '#{z["kind"]}' at '$.lanes[#{i}]' was folded into 'machine' (ADR 0091) — " \
39
41
  "use `kind: machine`",
40
42
  )
41
43
  end
42
44
 
43
45
  raise BadManifest.new(
44
- "unknown zone kind '#{z["kind"]}' at '$.zones[#{i}]' (known: #{ZONE_KINDS.join(", ")})",
46
+ "unknown lane kind '#{z["kind"]}' at '$.lanes[#{i}]' (known: #{LANE_KINDS.join(", ")})",
45
47
  )
46
48
  end
47
49
  end
@@ -168,7 +170,7 @@ module Textus
168
170
  hints = {
169
171
  "lifecycle" => "age GC moved to the `retention:` rule ({ ttl, action: drop|archive }); " \
170
172
  "intake cadence to the entry's `source: { ttl }`",
171
- "materialize" => "moved to the entry's `source: { on_write: sync|async }`",
173
+ "materialize" => "removed materialization is automatic (a write enqueues a job; run `drain`)",
172
174
  }
173
175
  hints.each do |old, hint|
174
176
  next unless rule.key?(old)
@@ -194,10 +196,10 @@ module Textus
194
196
  Array(r["can"]).each do |verb|
195
197
  next if CAPABILITIES.include?(verb)
196
198
 
197
- # The quarantine capability folded into reconcile (ADR 0090); a
199
+ # The quarantine capability folded into the converge capability (ADR 0090); a
198
200
  # manifest still naming the old quarantine capability (`ingest`, or
199
201
  # legacy `fetch`) gets a pointed hint rather than a bare error.
200
- hint = %w[ingest fetch].include?(verb) ? " — the quarantine capability folded into 'reconcile' (ADR 0090)" : ""
202
+ hint = %w[ingest fetch].include?(verb) ? " — the quarantine capability folded into 'converge' (ADR 0090)" : ""
201
203
  raise BadManifest.new(
202
204
  "unknown capability '#{verb}' for role '#{name}' at '#{path}' " \
203
205
  "(known: #{CAPABILITIES.join(", ")})#{hint}",
@@ -217,9 +219,9 @@ module Textus
217
219
  # (ADR 0045 D1) so attribution can't bypass the closed-name guarantee.
218
220
  # Applies to both zone owners and entry owners; owner is optional, so a
219
221
  # nil owner is not an error.
220
- def validate_owners!(zones, entries)
221
- Array(zones).each_with_index do |z, i|
222
- check_owner!(z["owner"], "$.zones[#{i}]")
222
+ def validate_owners!(lanes, entries)
223
+ Array(lanes).each_with_index do |z, i|
224
+ check_owner!(z["owner"], "$.lanes[#{i}]")
223
225
  end
224
226
  Array(entries).each_with_index do |e, i|
225
227
  check_owner!(e["owner"], "$.entries[#{i}]")
@@ -265,20 +267,20 @@ module Textus
265
267
  end
266
268
 
267
269
  def validate_single_queue!(raw)
268
- queues = Array(raw["zones"]).select { |z| z["kind"] == "queue" }.map { |z| z["name"] }
270
+ queues = Array(raw["lanes"]).select { |z| z["kind"] == "queue" }.map { |z| z["name"] }
269
271
  return if queues.size <= 1
270
272
 
271
273
  raise BadManifest.new(
272
- "at most one zone may declare kind: queue (found: #{queues.join(", ")})",
274
+ "at most one lane may declare kind: queue (found: #{queues.join(", ")})",
273
275
  )
274
276
  end
275
277
 
276
278
  def validate_single_machine!(raw)
277
- machines = Array(raw["zones"]).select { |z| z["kind"] == "machine" }.map { |z| z["name"] }
279
+ machines = Array(raw["lanes"]).select { |z| z["kind"] == "machine" }.map { |z| z["name"] }
278
280
  return if machines.size <= 1
279
281
 
280
282
  raise BadManifest.new(
281
- "at most one zone may declare kind: machine (found: #{machines.join(", ")})",
283
+ "at most one lane may declare kind: machine (found: #{machines.join(", ")})",
282
284
  )
283
285
  end
284
286
 
@@ -300,20 +302,20 @@ module Textus
300
302
  end
301
303
  end
302
304
 
303
- # Write authority is derived from capabilities (ADR 0030): a zone of a
305
+ # Write authority is derived from capabilities (ADR 0030): a lane of a
304
306
  # given kind can only be written by a role that holds the kind's required
305
- # verb. Reject a manifest declaring a zone whose required verb is held by
307
+ # verb. Reject a manifest declaring a lane whose required verb is held by
306
308
  # no role. Capabilities.resolve returns the defaults when `roles:` is nil,
307
309
  # so the capability union is all four verbs and every kind is satisfied.
308
- def validate_zone_kind_consistency!(raw)
310
+ def validate_lane_kind_consistency!(raw)
309
311
  held = Capabilities.resolve(raw["roles"]).values.flatten.uniq
310
312
 
311
- Array(raw["zones"]).each_with_index do |z, i|
313
+ Array(raw["lanes"]).each_with_index do |z, i|
312
314
  verb = KIND_REQUIRES_VERB[z["kind"]]
313
315
  next if verb.nil? || held.include?(verb)
314
316
 
315
317
  raise BadManifest.new(
316
- "zone '#{z["name"]}' (#{z["kind"]}) at '$.zones[#{i}]' " \
318
+ "lane '#{z["name"]}' (#{z["kind"]}) at '$.lanes[#{i}]' " \
317
319
  "needs a role with capability '#{verb}'; none declared",
318
320
  )
319
321
  end
@@ -5,17 +5,17 @@ module Textus
5
5
  # 0034; the quarantine + derived ZONE-KINDS folded into one `machine` kind
6
6
  # in ADR 0091). Each kind pairs with the capability that authorizes
7
7
  # originating bytes in it. ONE source of truth; the derived constants below
8
- # cannot drift. A BIJECTION again (0090 had two kinds → reconcile; 0091
8
+ # cannot drift. A BIJECTION again (0090 had two kinds → the converge capability; 0091
9
9
  # collapses them, so kind ↔ capability is 1:1).
10
10
  module Vocabulary
11
11
  LANES = {
12
12
  "canon" => "author",
13
13
  "workspace" => "keep",
14
- "machine" => "reconcile",
14
+ "machine" => "converge",
15
15
  "queue" => "propose",
16
16
  }.freeze
17
17
 
18
- ZONE_KINDS = LANES.keys.freeze
18
+ LANE_KINDS = LANES.keys.freeze
19
19
  CAPABILITIES = LANES.values.uniq.freeze
20
20
  KIND_REQUIRES_VERB = LANES
21
21
  end
@@ -7,13 +7,13 @@ module Textus
7
7
  module Schema
8
8
  # Re-export the vocabulary.
9
9
  LANES = Vocabulary::LANES
10
- ZONE_KINDS = Vocabulary::ZONE_KINDS
10
+ LANE_KINDS = Vocabulary::LANE_KINDS
11
11
  CAPABILITIES = Vocabulary::CAPABILITIES
12
12
  KIND_REQUIRES_VERB = Vocabulary::KIND_REQUIRES_VERB
13
13
  # Re-export the keys + registry.
14
14
  ROOT_KEYS = Keys::ROOT_KEYS
15
15
  ROLE_KEYS = Keys::ROLE_KEYS
16
- ZONE_KEYS = Keys::ZONE_KEYS
16
+ LANE_KEYS = Keys::LANE_KEYS
17
17
  ENTRY_KEYS = Keys::ENTRY_KEYS
18
18
  PUBLISH_KEYS = Keys::PUBLISH_KEYS
19
19
  SOURCE_KEYS = Keys::SOURCE_KEYS
@@ -6,8 +6,8 @@ module Textus
6
6
  #
7
7
  # * data — frozen value: raw, root, zones, entries, audit_config, role_caps
8
8
  # * resolver — resolves keys → entry + path
9
- # * policy — zone/role authority (zone_writers, declared_kind, derived_entry?,
10
- # queue_zone?, permission_for, …)
9
+ # * policy — zone/role authority (lane_writers, declared_kind, derived_entry?,
10
+ # queue_lane?, permission_for, …)
11
11
  # * rules — match-block rule engine (lifecycle, handler allowlist, materialize, …)
12
12
  #
13
13
  # Use `manifest.data.entries`, `manifest.policy.declared_kind(z)`, etc.
@@ -1,12 +1,12 @@
1
1
  require "timeout"
2
2
 
3
3
  module Textus
4
- module Produce
4
+ module Pipeline
5
5
  module Acquire
6
6
  # Invokes a :resolve_handler hook handler by name under a timeout — the single
7
7
  # home for "call the intake handler under a deadline" (ADR 0048 D1). Shared by
8
8
  # Produce::Acquire::Intake (the internal ingest mechanism — no public verb since ADR 0079)
9
- # as driven by the `reconcile` sweep and `textus hook run` (ADR 0089 made
9
+ # as driven by the converge sweep (drain/serve) and `textus hook run` (ADR 0089 made
10
10
  # ingest system-pushed; there is no read or put trigger).
11
11
  # Always passes a Container as `caps:` so the hook contract (ADR 0027) is
12
12
  # uniform across every entry point. Maps Timeout::Error to a UsageError;
@@ -18,7 +18,7 @@ module Textus
18
18
 
19
19
  def invoke(caps:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
20
20
  Timeout.timeout(timeout) do
21
- caps.rpc.invoke(:resolve_handler, handler, caps: caps, config: config, args: args)
21
+ caps.steps.invoke(:fetch, handler, caps: caps, config: config, args: args)
22
22
  end
23
23
  rescue Timeout::Error
24
24
  raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")