textus 0.52.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +62 -187
  5. data/docs/architecture/README.md +88 -77
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +53 -0
  8. data/lib/textus/action/audit.rb +133 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +30 -22
  11. data/lib/textus/action/boot.rb +26 -0
  12. data/lib/textus/action/data_mv.rb +71 -0
  13. data/lib/textus/action/deps.rb +48 -0
  14. data/lib/textus/action/doctor.rb +26 -0
  15. data/lib/textus/action/drain.rb +41 -0
  16. data/lib/textus/action/enqueue.rb +55 -0
  17. data/lib/textus/action/get.rb +80 -0
  18. data/lib/textus/action/jobs.rb +38 -0
  19. data/lib/textus/action/key_delete.rb +46 -0
  20. data/lib/textus/action/key_delete_prefix.rb +46 -0
  21. data/lib/textus/action/key_mv.rb +143 -0
  22. data/lib/textus/action/key_mv_prefix.rb +59 -0
  23. data/lib/textus/action/list.rb +44 -0
  24. data/lib/textus/action/propose.rb +54 -0
  25. data/lib/textus/action/published.rb +26 -0
  26. data/lib/textus/action/pulse/scanner.rb +118 -0
  27. data/lib/textus/action/pulse.rb +87 -0
  28. data/lib/textus/action/put.rb +63 -0
  29. data/lib/textus/action/rdeps.rb +49 -0
  30. data/lib/textus/action/reject.rb +49 -0
  31. data/lib/textus/action/rule_explain.rb +95 -0
  32. data/lib/textus/action/rule_lint.rb +70 -0
  33. data/lib/textus/action/rule_list.rb +46 -0
  34. data/lib/textus/action/schema_envelope.rb +31 -0
  35. data/lib/textus/action/uid.rb +35 -0
  36. data/lib/textus/action/where.rb +38 -0
  37. data/lib/textus/action/write_verb.rb +58 -0
  38. data/lib/textus/background/job/base.rb +27 -0
  39. data/lib/textus/background/job/materialize.rb +31 -0
  40. data/lib/textus/background/job/refresh.rb +22 -0
  41. data/lib/textus/background/job/sweep.rb +31 -0
  42. data/lib/textus/background/job.rb +19 -0
  43. data/lib/textus/background/plan.rb +9 -0
  44. data/lib/textus/background/planner/plan.rb +113 -0
  45. data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
  46. data/lib/textus/background/worker.rb +67 -0
  47. data/lib/textus/boot.rb +53 -45
  48. data/lib/textus/command.rb +36 -0
  49. data/lib/textus/container.rb +1 -1
  50. data/lib/textus/{domain → core}/duration.rb +1 -1
  51. data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
  52. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  53. data/lib/textus/{domain → core}/freshness.rb +2 -2
  54. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  55. data/lib/textus/{domain → core}/retention.rb +2 -2
  56. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  57. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  58. data/lib/textus/doctor/check/handler_permit.rb +34 -0
  59. data/lib/textus/doctor/check/hooks.rb +11 -18
  60. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  61. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  62. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  63. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  64. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  65. data/lib/textus/doctor/check.rb +12 -9
  66. data/lib/textus/{read → doctor}/validator.rb +22 -13
  67. data/lib/textus/doctor.rb +6 -6
  68. data/lib/textus/envelope/io/writer.rb +65 -36
  69. data/lib/textus/envelope.rb +5 -3
  70. data/lib/textus/errors.rb +17 -9
  71. data/lib/textus/events.rb +21 -0
  72. data/lib/textus/gate/auth.rb +181 -0
  73. data/lib/textus/gate.rb +114 -0
  74. data/lib/textus/init/templates/machine_intake.rb +39 -35
  75. data/lib/textus/init/templates/orientation_reducer.rb +15 -11
  76. data/lib/textus/init.rb +90 -73
  77. data/lib/textus/key/path.rb +9 -2
  78. data/lib/textus/layout.rb +13 -0
  79. data/lib/textus/manifest/data.rb +14 -14
  80. data/lib/textus/manifest/entry/base.rb +15 -11
  81. data/lib/textus/manifest/entry/parser.rb +6 -6
  82. data/lib/textus/manifest/entry/produced.rb +3 -2
  83. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  84. data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
  85. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  86. data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
  87. data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
  88. data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
  89. data/lib/textus/manifest/policy/react.rb +30 -0
  90. data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
  91. data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
  92. data/lib/textus/manifest/policy.rb +36 -48
  93. data/lib/textus/manifest/resolver.rb +3 -2
  94. data/lib/textus/manifest/rules.rb +4 -4
  95. data/lib/textus/manifest/schema/keys.rb +17 -11
  96. data/lib/textus/manifest/schema/validator.rb +24 -22
  97. data/lib/textus/manifest/schema/vocabulary.rb +1 -1
  98. data/lib/textus/manifest/schema.rb +2 -2
  99. data/lib/textus/manifest.rb +2 -2
  100. data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
  101. data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
  102. data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
  103. data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
  104. data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
  105. data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
  106. data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
  107. data/lib/textus/{produce → pipeline}/engine.rb +7 -5
  108. data/lib/textus/{produce → pipeline}/render.rb +3 -1
  109. data/lib/textus/ports/audit_log.rb +31 -5
  110. data/lib/textus/ports/audit_subscriber.rb +4 -4
  111. data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
  112. data/lib/textus/ports/queue.rb +1 -1
  113. data/lib/textus/ports/sentinel_store.rb +2 -2
  114. data/lib/textus/ports/watcher_lock.rb +48 -0
  115. data/lib/textus/projection.rb +8 -8
  116. data/lib/textus/schema/tools.rb +4 -3
  117. data/lib/textus/session.rb +6 -3
  118. data/lib/textus/step/base.rb +35 -0
  119. data/lib/textus/step/builtin/csv_fetch.rb +19 -0
  120. data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
  121. data/lib/textus/step/builtin/json_fetch.rb +18 -0
  122. data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
  123. data/lib/textus/step/builtin/rss_fetch.rb +26 -0
  124. data/lib/textus/step/builtin.rb +22 -0
  125. data/lib/textus/{hooks → step}/catalog.rb +3 -3
  126. data/lib/textus/{hooks → step}/context.rb +15 -13
  127. data/lib/textus/step/discovery.rb +24 -0
  128. data/lib/textus/{hooks → step}/error_log.rb +1 -1
  129. data/lib/textus/{hooks → step}/event_bus.rb +15 -16
  130. data/lib/textus/step/fetch.rb +13 -0
  131. data/lib/textus/{hooks → step}/fire_report.rb +1 -1
  132. data/lib/textus/step/loader.rb +108 -0
  133. data/lib/textus/step/observe.rb +31 -0
  134. data/lib/textus/step/registry_store.rb +66 -0
  135. data/lib/textus/{hooks → step}/signature.rb +1 -1
  136. data/lib/textus/step/transform.rb +12 -0
  137. data/lib/textus/step/validate.rb +11 -0
  138. data/lib/textus/step.rb +10 -0
  139. data/lib/textus/store.rb +17 -15
  140. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  141. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  142. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  143. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  144. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  145. data/lib/textus/surfaces/cli/group.rb +50 -0
  146. data/lib/textus/surfaces/cli/runner.rb +236 -0
  147. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  148. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  149. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  150. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  151. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  152. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  153. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  154. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  155. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  156. data/lib/textus/surfaces/cli/verb.rb +111 -0
  157. data/lib/textus/surfaces/cli.rb +148 -0
  158. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  159. data/lib/textus/surfaces/mcp/errors.rb +34 -0
  160. data/lib/textus/surfaces/mcp/server.rb +145 -0
  161. data/lib/textus/surfaces/mcp/session.rb +9 -0
  162. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  163. data/lib/textus/surfaces/mcp.rb +8 -0
  164. data/lib/textus/surfaces/role_scope.rb +38 -0
  165. data/lib/textus/surfaces/watcher.rb +38 -0
  166. data/lib/textus/version.rb +1 -1
  167. data/lib/textus.rb +64 -22
  168. metadata +132 -118
  169. data/lib/textus/cli/group/hook.rb +0 -9
  170. data/lib/textus/cli/group/key.rb +0 -9
  171. data/lib/textus/cli/group/mcp.rb +0 -9
  172. data/lib/textus/cli/group/rule.rb +0 -9
  173. data/lib/textus/cli/group/schema.rb +0 -9
  174. data/lib/textus/cli/group/zone.rb +0 -9
  175. data/lib/textus/cli/group.rb +0 -48
  176. data/lib/textus/cli/runner.rb +0 -193
  177. data/lib/textus/cli/verb/doctor.rb +0 -17
  178. data/lib/textus/cli/verb/get.rb +0 -18
  179. data/lib/textus/cli/verb/hook_run.rb +0 -48
  180. data/lib/textus/cli/verb/hooks.rb +0 -50
  181. data/lib/textus/cli/verb/init.rb +0 -18
  182. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  183. data/lib/textus/cli/verb/put.rb +0 -30
  184. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  185. data/lib/textus/cli/verb/schema_init.rb +0 -19
  186. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  187. data/lib/textus/cli/verb/serve.rb +0 -19
  188. data/lib/textus/cli/verb.rb +0 -116
  189. data/lib/textus/cli.rb +0 -138
  190. data/lib/textus/dispatcher.rb +0 -54
  191. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  192. data/lib/textus/domain/action.rb +0 -9
  193. data/lib/textus/domain/jobs/registry.rb +0 -37
  194. data/lib/textus/domain/permission.rb +0 -7
  195. data/lib/textus/domain/policy/base_guards.rb +0 -25
  196. data/lib/textus/domain/policy/evaluation.rb +0 -15
  197. data/lib/textus/domain/policy/guard.rb +0 -35
  198. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  199. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  200. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  201. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  202. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  203. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  204. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  205. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  206. data/lib/textus/hooks/builtin.rb +0 -70
  207. data/lib/textus/hooks/loader.rb +0 -54
  208. data/lib/textus/hooks/rpc_registry.rb +0 -43
  209. data/lib/textus/jobs/handlers.rb +0 -62
  210. data/lib/textus/jobs/scheduler.rb +0 -36
  211. data/lib/textus/jobs/seeder.rb +0 -57
  212. data/lib/textus/maintenance/drain.rb +0 -42
  213. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  214. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  215. data/lib/textus/maintenance/rule_lint.rb +0 -66
  216. data/lib/textus/maintenance/serve.rb +0 -30
  217. data/lib/textus/maintenance/worker.rb +0 -74
  218. data/lib/textus/maintenance/zone_mv.rb +0 -64
  219. data/lib/textus/maintenance.rb +0 -15
  220. data/lib/textus/mcp/catalog.rb +0 -70
  221. data/lib/textus/mcp/errors.rb +0 -32
  222. data/lib/textus/mcp/server.rb +0 -138
  223. data/lib/textus/mcp/session.rb +0 -7
  224. data/lib/textus/mcp/tool_schemas.rb +0 -15
  225. data/lib/textus/mcp.rb +0 -6
  226. data/lib/textus/mustache.rb +0 -117
  227. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  228. data/lib/textus/produce/events.rb +0 -36
  229. data/lib/textus/read/audit.rb +0 -130
  230. data/lib/textus/read/boot.rb +0 -26
  231. data/lib/textus/read/capabilities.rb +0 -70
  232. data/lib/textus/read/deps.rb +0 -38
  233. data/lib/textus/read/doctor.rb +0 -27
  234. data/lib/textus/read/freshness.rb +0 -152
  235. data/lib/textus/read/get.rb +0 -73
  236. data/lib/textus/read/jobs.rb +0 -31
  237. data/lib/textus/read/list.rb +0 -24
  238. data/lib/textus/read/published.rb +0 -22
  239. data/lib/textus/read/pulse.rb +0 -98
  240. data/lib/textus/read/rdeps.rb +0 -39
  241. data/lib/textus/read/rule_explain.rb +0 -96
  242. data/lib/textus/read/rule_list.rb +0 -54
  243. data/lib/textus/read/schema_envelope.rb +0 -25
  244. data/lib/textus/read/uid.rb +0 -29
  245. data/lib/textus/read/validate_all.rb +0 -36
  246. data/lib/textus/read/where.rb +0 -24
  247. data/lib/textus/role_scope.rb +0 -78
  248. data/lib/textus/write/accept.rb +0 -58
  249. data/lib/textus/write/enqueue.rb +0 -50
  250. data/lib/textus/write/key_delete.rb +0 -65
  251. data/lib/textus/write/key_mv.rb +0 -141
  252. data/lib/textus/write/propose.rb +0 -54
  253. data/lib/textus/write/put.rb +0 -74
  254. data/lib/textus/write/reject.rb +0 -68
@@ -2,14 +2,14 @@ module Textus
2
2
  class Manifest
3
3
  class Entry
4
4
  class Base < Entry
5
- attr_reader :raw, :key, :path, :zone, :schema, :owner, :format, :publish_targets
5
+ attr_reader :raw, :key, :path, :lane, :schema, :owner, :format, :publish_targets
6
6
 
7
7
  # rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
8
- def initialize(raw:, key:, path:, zone:, schema:, owner:, format:, publish_targets: [])
8
+ def initialize(raw:, key:, path:, lane:, schema:, owner:, format:, publish_targets: [])
9
9
  @raw = raw
10
10
  @key = key
11
11
  @path = path
12
- @zone = zone
12
+ @lane = lane
13
13
  @schema = schema
14
14
  @owner = owner
15
15
  @format = format
@@ -17,13 +17,14 @@ module Textus
17
17
  end
18
18
  # rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
19
19
 
20
- def zone_writers(policy)
21
- policy.zone_writers(@zone)
20
+ def lane_writers(policy)
21
+ verb = policy.verb_for_lane(@lane)
22
+ policy.roles_with_capability(verb)
22
23
  rescue UsageError => e
23
24
  raise UsageError.new("entry '#{@key}': #{e.message}")
24
25
  end
25
26
 
26
- def in_proposal_zone?(policy) = policy.queue_zone?(@zone)
27
+ def in_proposal_lane?(policy) = policy.queue_lane?(@lane)
27
28
 
28
29
  def nested? = false
29
30
  def derived? = false
@@ -36,6 +37,9 @@ module Textus
36
37
  def external? = false
37
38
  def projection? = false
38
39
 
40
+ alias zone lane
41
+ alias in_proposal_zone? in_proposal_lane?
42
+
39
43
  # Whether git should track this entry's file. Default true; an entry
40
44
  # marked `tracked: false` in the manifest stays protocol-readable but is
41
45
  # listed in the generated `.gitignore` (ADR 0043). Cross-cutting, so it
@@ -60,19 +64,19 @@ module Textus
60
64
  # Minimal context object passed into entry `publish_via` hooks.
61
65
  # Everything beyond the three primitives is derived. Data.define
62
66
  # instances are frozen, so we recompute per-call rather than
63
- # memoizing — RoleScope/Hooks::Context construction is cheap.
67
+ # memoizing — RoleScope/Step::Context construction is cheap.
64
68
  PublishContext = ::Data.define(:container, :call, :reader) do
65
69
  def manifest = container.manifest
66
70
  def root = container.root
67
71
  def repo_root = File.dirname(container.root)
68
- def events = container.events
72
+ def steps = container.steps
69
73
 
70
74
  def hook_context
71
- Textus::Hooks::Context.new(scope: scope_for_hooks)
75
+ Textus::Step::Context.new(scope: scope_for_hooks)
72
76
  end
73
77
 
74
78
  def emit(event, **payload)
75
- events.publish(event, ctx: hook_context, **payload)
79
+ steps.publish(event, ctx: hook_context, **payload)
76
80
  end
77
81
 
78
82
  # Read a named template from the store's templates/ directory.
@@ -91,7 +95,7 @@ module Textus
91
95
  private
92
96
 
93
97
  def scope_for_hooks
94
- Textus::RoleScope.new(
98
+ Textus::Surfaces::RoleScope.new(
95
99
  container: container, role: call.role, dry_run: call.dry_run,
96
100
  )
97
101
  end
@@ -5,7 +5,7 @@ module Textus
5
5
  def self.call(raw)
6
6
  key = raw["key"] or raise UsageError.new("manifest entry missing key")
7
7
  path = raw["path"] or raise UsageError.new("manifest entry '#{key}' missing path")
8
- zone = raw["zone"] or raise UsageError.new("manifest entry '#{key}' missing zone")
8
+ lane = raw["lane"] or raise UsageError.new("manifest entry '#{key}' missing lane")
9
9
 
10
10
  raw_kind = raw["kind"] or raise BadManifest.new("entry '#{key}' missing required `kind:` (#{Entry::REGISTRY.keys.join("|")})")
11
11
  kind = raw_kind.to_sym
@@ -19,7 +19,7 @@ module Textus
19
19
 
20
20
  common = {
21
21
  raw: raw,
22
- key: key, path: path, zone: zone,
22
+ key: key, path: path, lane: lane,
23
23
  schema: raw["schema"], owner: raw["owner"],
24
24
  format: format,
25
25
  publish_targets: publish_targets(raw)
@@ -31,12 +31,12 @@ module Textus
31
31
  end
32
32
 
33
33
  # ADR 0093: an entry's production block is the unified `source:`. Returns a
34
- # Domain::Policy::Source; kind (intake/derived) is read from source.from.
34
+ # Manifest::Policy::Source; kind (intake/derived) is read from source.from.
35
35
  def self.parse_source(raw, key)
36
36
  block = raw["source"] or
37
- raise BadManifest.new("entry '#{key}' requires a source: { from: project|handler|command, ... }")
37
+ raise BadManifest.new("entry '#{key}' requires a source: { from: derive|fetch|external, ... }")
38
38
 
39
- Textus::Domain::Policy::Source.new(block)
39
+ Textus::Manifest::Policy::Source.new(block)
40
40
  end
41
41
 
42
42
  # ADR 0094: `publish:` is a LIST of target objects — to-targets
@@ -52,7 +52,7 @@ module Textus
52
52
  "[{to:, template:?} | {tree:}] (ADR 0094); the `publish: { … }` map form was retired",
53
53
  )
54
54
  end
55
- block.map { |t| Textus::Domain::Policy::PublishTarget.new(t) }
55
+ block.map { |t| Textus::Manifest::Policy::PublishTarget.new(t) }
56
56
  end
57
57
 
58
58
  def self.resolve_format(raw, path)
@@ -17,6 +17,8 @@ module Textus
17
17
  def derived? = @source.kind == :derived
18
18
  def external? = @source.external?
19
19
  def projection? = @source.projection?
20
+ def fetch? = @source.fetch?
21
+ def derive? = @source.derive?
20
22
  def nested? = !!@raw["nested"]
21
23
  def handler = @source.handler
22
24
  def config = @source.config
@@ -33,11 +35,10 @@ module Textus
33
35
  def publish_via(pctx, prefix: nil)
34
36
  built = false
35
37
  if projection?
36
- Textus::Produce::Acquire::Projection.new(container: pctx.container, call: pctx.call).run(self)
38
+ Textus::Pipeline::Acquire::Projection.new(container: pctx.container, call: pctx.call).run(self)
37
39
  built = true
38
40
  pctx.emit(:entry_produced, key: @key, envelope: pctx.reader.call(@key), sources: Array(@source.select).compact)
39
41
  end
40
-
41
42
  emitted = publish_mode.publish(pctx, prefix: prefix)
42
43
  return emitted if emitted
43
44
  return nil unless built
@@ -36,7 +36,7 @@ module Textus
36
36
 
37
37
  # Store-side directory this entry's tree lives under.
38
38
  def store_base(pctx)
39
- File.join(pctx.root, "zones", entry.path)
39
+ File.join(pctx.root, Textus::Key::Path.normalize_relative_path(entry.path))
40
40
  end
41
41
  end
42
42
  end
@@ -16,11 +16,12 @@ module Textus
16
16
 
17
17
  def publish(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/AbcSize
18
18
  targets = entry.publish_targets.select(&:to_target?)
19
+
19
20
  return nil if targets.empty?
20
21
 
21
22
  data_path = pctx.manifest.resolver.resolve(entry.key).path
22
23
  envelope = pctx.reader.call(entry.key)
23
- renderer = Textus::Produce::Render.new(template_loader: ->(n) { pctx.read_template(n) })
24
+ renderer = Textus::Pipeline::Render.new(template_loader: ->(n) { pctx.read_template(n) })
24
25
  content = nil # parsed lazily; the data's `content` (always _meta-free)
25
26
 
26
27
  targets.each do |t|
@@ -4,7 +4,7 @@ module Textus
4
4
  module Validators
5
5
  module Events
6
6
  def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
7
- pubsub_events = Textus::Hooks::Catalog::PUBSUB.keys
7
+ pubsub_events = Textus::Step::Catalog::PUBSUB.keys
8
8
  events = entry.events
9
9
  events.each_key do |evt|
10
10
  next if pubsub_events.include?(evt.to_sym)
@@ -1,14 +1,14 @@
1
1
  module Textus
2
- module Domain
3
- module Policy
4
- class HandlerAllowlist
2
+ class Manifest
3
+ class Policy
4
+ class HandlerPermit
5
5
  attr_reader :handlers
6
6
 
7
7
  def initialize(handlers:)
8
8
  @handlers = Array(handlers).map(&:to_s).freeze
9
9
  end
10
10
 
11
- def allows?(handler)
11
+ def permits?(handler)
12
12
  @handlers.include?(handler.to_s)
13
13
  end
14
14
  end
@@ -1,6 +1,6 @@
1
1
  module Textus
2
- module Domain
3
- module Policy
2
+ class Manifest
3
+ class Policy
4
4
  module Matcher
5
5
  module_function
6
6
 
@@ -1,6 +1,6 @@
1
1
  module Textus
2
- module Domain
3
- module Policy
2
+ class Manifest
3
+ class Policy
4
4
  # One publish destination (ADR 0094). Exactly one of:
5
5
  # to-target { to:, template:?, inject_boot:? } — render data through a
6
6
  # template, or copy verbatim when no template
@@ -0,0 +1,30 @@
1
+ module Textus
2
+ class Manifest
3
+ class Policy
4
+ class React
5
+ ALLOWED_KEYS = %w[on when do scope budget idempotency observe priority].freeze
6
+
7
+ attr_reader :raw
8
+
9
+ def initialize(raw:)
10
+ raise Textus::BadManifest.new("react: must be a map") unless raw.is_a?(Hash)
11
+
12
+ raw = raw.each_with_object({}) do |(key, value), out|
13
+ normalized = key == true ? "on" : key.to_s
14
+ out[normalized] = value
15
+ end
16
+ raise Textus::BadManifest.new("react.ttl is invalid; ttl belongs only to source.ttl or retention.ttl") if raw.key?("ttl")
17
+
18
+ unknown = raw.keys - ALLOWED_KEYS
19
+ raise Textus::BadManifest.new("react: unknown key(s): #{unknown.join(", ")}") unless unknown.empty?
20
+
21
+ @raw = raw
22
+ end
23
+
24
+ def to_h
25
+ @raw
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,6 +1,6 @@
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
6
  # `converge` pass, never on a write (ADR 0079's invariant). Orthogonal to
@@ -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)
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
10
  # Materialization is async-only (job-queue model): a write enqueues a
11
11
  # `materialize` job, converged by a worker. There is no per-entry write
12
12
  # trigger knob.
13
13
  class Source
14
- FROMS = %w[project handler command].freeze
14
+ FROMS = %w[fetch derive external].freeze
15
15
 
16
16
  attr_reader :from, :handler, :config, :command, :sources
17
17
 
@@ -25,16 +25,21 @@ module Textus
25
25
  @projection = {}
26
26
 
27
27
  case @from
28
- when "project" then init_project(raw)
29
- when "handler" then init_handler(raw)
30
- 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)
31
31
  end
32
32
  end
33
33
 
34
- def kind = @from == "handler" ? :intake : :derived
35
- def external? = @from == "command"
36
- def projection? = @from == "project"
37
- 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)
38
43
 
39
44
  # Flattened projection accessors (ADR 0094) — read directly off the source
40
45
  # block; nil when absent or not a projection source.
@@ -49,22 +54,22 @@ module Textus
49
54
 
50
55
  private
51
56
 
52
- def init_project(raw)
57
+ def init_derive(raw)
53
58
  %w[select pluck sort_by transform].each { |f| @projection[f] = raw[f] if raw.key?(f) }
54
59
  return unless @projection["select"].nil? && @projection["transform"].nil?
55
60
 
56
- 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:`")
57
62
  end
58
63
 
59
- def init_handler(raw)
64
+ def init_fetch(raw)
60
65
  @handler = raw["handler"] or
61
- raise Textus::BadManifest.new("source (from: handler) requires a `handler:` field")
66
+ raise Textus::BadManifest.new("source (from: fetch) requires a `handler:` field")
62
67
  @config = raw["config"] || {}
63
68
  end
64
69
 
65
- def init_command(raw)
70
+ def init_external(raw)
66
71
  @command = raw["command"] or
67
- raise Textus::BadManifest.new("source (from: command) requires a `command:` field")
72
+ raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
68
73
  @sources = raw["sources"] || []
69
74
  end
70
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
@@ -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