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
@@ -6,15 +6,15 @@ module Textus
6
6
 
7
7
  def call
8
8
  declared = collect_declared_handlers
9
- registered = rpc.names(:resolve_handler).to_set
9
+ registered = steps.names(:fetch).to_set
10
10
 
11
11
  out = (declared - registered).map do |name|
12
12
  {
13
13
  "code" => "intake.handler_missing",
14
14
  "level" => "error",
15
15
  "subject" => name.to_s,
16
- "message" => "manifest references intake handler '#{name}' but no resolve_handler hook for '#{name}' is registered",
17
- "fix" => "create .textus/hooks/#{name}.rb with `Textus.hook { |reg| reg.on(:resolve_handler, :#{name}) { ... } }`",
16
+ "message" => "manifest references intake handler '#{name}' but no fetch step for '#{name}' is registered",
17
+ "fix" => "create .textus/steps/fetch/#{name}.rb with `class #{name.to_s.split(/[-_]/).map(&:capitalize).join}Fetch < Textus::Step::Fetch`",
18
18
  }
19
19
  end
20
20
 
@@ -23,8 +23,8 @@ module Textus
23
23
  "code" => "intake.handler_orphan",
24
24
  "level" => "warning",
25
25
  "subject" => name.to_s,
26
- "message" => "resolve_handler hook '#{name}' is registered but no manifest entry references it",
27
- "fix" => "remove the unused handler, or add an entry with `intake.handler: #{name}`",
26
+ "message" => "fetch step '#{name}' is registered but no manifest entry references it",
27
+ "fix" => "remove the unused step, or add an entry with `intake.handler: #{name}`",
28
28
  }
29
29
  end
30
30
 
@@ -8,10 +8,10 @@ module Textus
8
8
  # corruption (the accept gate already refuses them).
9
9
  class ProposalTargets < Check
10
10
  def call
11
- queue = manifest.policy.queue_zone
11
+ queue = manifest.policy.queue_lane
12
12
  return [] unless queue
13
13
 
14
- dispatch(:list, zone: queue).filter_map { |row| issue_for(row["key"]) }
14
+ dispatch(:list, lane: queue).filter_map { |row| issue_for(row["key"]) }
15
15
  end
16
16
 
17
17
  private
@@ -20,7 +20,7 @@ module Textus
20
20
  target = dispatch(:get, key).meta&.dig("proposal", "target_key")
21
21
  return nil if target.nil? # not a proposal entry — skip
22
22
 
23
- zone = manifest.resolver.resolve(target).entry.zone
23
+ zone = manifest.resolver.resolve(target).entry.lane
24
24
  return nil if manifest.policy.declared_kind(zone.to_s) == :canon
25
25
 
26
26
  {
@@ -2,7 +2,7 @@ module Textus
2
2
  module Doctor
3
3
  class Check
4
4
  # Flags entries whose key is matched by two or more rule blocks of the
5
- # SAME specificity in the same slot (lifecycle / handler_allowlist /
5
+ # SAME specificity in the same slot (lifecycle / handler_permit /
6
6
  # guard / materialize). Ties are non-deterministic in the parser's pick step, so
7
7
  # they're a configuration smell — surface them.
8
8
  class RuleAmbiguity < Check
@@ -26,7 +26,7 @@ module Textus
26
26
  carriers = matches.select { |b| b.public_send(slot) }
27
27
  return [] if carriers.length < 2
28
28
 
29
- by_specificity = carriers.group_by { |b| Textus::Domain::Policy::Matcher.specificity(b.match) }
29
+ by_specificity = carriers.group_by { |b| Textus::Manifest::Policy::Matcher.specificity(b.match) }
30
30
  tied = by_specificity.values.select { |group| group.length > 1 }
31
31
  tied.map { |group| issue_for(mentry, slot, group) }
32
32
  end
@@ -3,8 +3,14 @@ module Textus
3
3
  class Check
4
4
  class SchemaViolations < Check
5
5
  def call
6
- res = dispatch(:validate_all)
7
- res["violations"].map do |v|
6
+ result = Textus::Doctor::Validator.new(
7
+ reader: ->(key, ctnr, c) { Textus::Action::Get.new(key: key).call(container: ctnr, call: c) },
8
+ manifest: @container.manifest,
9
+ audit_log: @container.audit_log,
10
+ schema_for: ->(name) { @container.schemas.fetch_or_nil(name) },
11
+ ).call(container: @container, call: Textus::Call.build(role: Textus::Role::DEFAULT))
12
+
13
+ result["violations"].map do |v|
8
14
  fix = v["expected"] &&
9
15
  "field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
10
16
  {
@@ -31,7 +31,7 @@ module Textus
31
31
  "level" => "warning",
32
32
  "subject" => sentinel_path,
33
33
  "message" => "sentinel is not valid JSON",
34
- "fix" => "delete #{sentinel_path} and re-run 'textus reconcile' to regenerate",
34
+ "fix" => "delete #{sentinel_path} and re-run 'textus drain' to regenerate",
35
35
  }
36
36
  end
37
37
 
@@ -51,7 +51,7 @@ module Textus
51
51
  "level" => "warning",
52
52
  "subject" => sentinel.target,
53
53
  "message" => "published file at #{sentinel.target} was modified out-of-band",
54
- "fix" => "re-run 'textus reconcile' to overwrite, or copy the manual edit back into the store source",
54
+ "fix" => "re-run 'textus drain' to overwrite, or copy the manual edit back into the store source",
55
55
  }
56
56
  end
57
57
  end
@@ -14,8 +14,9 @@ module Textus
14
14
  .downcase
15
15
  end
16
16
 
17
- def initialize(container)
17
+ def initialize(container, role: Textus::Role::DEFAULT)
18
18
  @container = container
19
+ @role = role
19
20
  end
20
21
 
21
22
  def call
@@ -26,16 +27,18 @@ module Textus
26
27
 
27
28
  def root = @container.root
28
29
  def manifest = @container.manifest
29
- def rpc = @container.rpc
30
+ def steps = @container.steps
30
31
 
31
- # Dispatch a verb through the single use-case invocation seam (ADR 0026).
32
+ # Dispatch a verb through Gate.
32
33
  def dispatch(verb, *args, **kwargs)
33
- Textus::Dispatcher.invoke(
34
- verb,
35
- container: @container,
36
- call: Textus::Call.build(role: Textus::Role::DEFAULT),
37
- args: args, kwargs: kwargs
38
- )
34
+ klass = Textus::Action::VERBS[verb]
35
+ spec = klass.contract if klass.respond_to?(:contract?) && klass.contract?
36
+ inputs = spec ? Textus::Contract::Binder.inputs_from_ordered(spec, args, kwargs) : kwargs
37
+ cmd_class = Textus::Gate::VERB_COMMAND.fetch(verb)
38
+ merged = inputs.merge(role: @role)
39
+ filled = cmd_class.members.to_h { |m| [m, merged.key?(m) ? merged[m] : nil] }
40
+ cmd = cmd_class.new(**filled)
41
+ @container.gate.dispatch(cmd)
39
42
  end
40
43
  end
41
44
  end
@@ -1,27 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Textus
2
- module Read
4
+ module Doctor
3
5
  class Validator
4
6
  def initialize(reader:, manifest:, audit_log:, schema_for:)
5
- @reader = reader
6
- @manifest = manifest
7
+ @reader = reader
8
+ @manifest = manifest
7
9
  @audit_log = audit_log
8
10
  @schema_for = schema_for
9
11
  end
10
12
 
11
- def call
13
+ def call(container:, call:)
14
+ @container = container
15
+ @call = call
12
16
  violations = []
13
17
  check_content_violations(violations)
14
18
  check_role_authority_violations(violations)
15
- { "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
19
+ { "protocol" => Textus::PROTOCOL, "ok" => violations.empty?, "violations" => violations }
16
20
  end
17
21
 
18
22
  private
19
23
 
20
24
  def check_content_violations(violations)
21
25
  @manifest.resolver.enumerate.each do |row|
22
- key = row[:key]
26
+ key = row[:key]
23
27
  mentry = row[:manifest_entry]
24
- env = fetch_envelope(key, violations) or next
28
+ env = fetch_envelope(key, violations) or next
25
29
  schema = mentry.schema && @schema_for.call(mentry.schema)
26
30
  next unless schema
27
31
 
@@ -42,7 +46,7 @@ module Textus
42
46
  next unless schema
43
47
 
44
48
  env = begin
45
- @reader.get(row[:key])
49
+ @reader.call(row[:key], @container, @call)
46
50
  rescue StandardError
47
51
  next
48
52
  end
@@ -54,19 +58,24 @@ module Textus
54
58
  last_writer = @audit_log.last_writer_for(key)
55
59
  return if last_writer.nil?
56
60
 
57
- last_writer_is_authority = @manifest.policy.roles_with_capability("author").include?(last_writer)
58
-
61
+ last_writer_is_authority =
62
+ @manifest.policy.roles_with_capability("author").include?(last_writer)
59
63
  env.meta.each_key do |field|
60
64
  owner = schema.maintained_by(field)
61
65
  next if owner.nil? || last_writer == owner || last_writer_is_authority
62
66
 
63
- violations << { "key" => key, "code" => "role_authority",
64
- "field" => field, "expected" => owner, "last_writer" => last_writer }
67
+ violations << {
68
+ "key" => key,
69
+ "code" => "role_authority",
70
+ "field" => field,
71
+ "expected" => owner,
72
+ "last_writer" => last_writer,
73
+ }
65
74
  end
66
75
  end
67
76
 
68
77
  def fetch_envelope(key, violations)
69
- @reader.get(key)
78
+ @reader.call(key, @container, @call)
70
79
  rescue Textus::Error => e
71
80
  violations << { "key" => key, "code" => e.code, "message" => e.message }
72
81
  nil
data/lib/textus/doctor.rb CHANGED
@@ -22,7 +22,7 @@ module Textus
22
22
  Check::UnownedSchemaFields,
23
23
  Check::SchemaViolations,
24
24
  Check::RuleAmbiguity,
25
- Check::HandlerAllowlist,
25
+ Check::HandlerPermit,
26
26
  Check::OrphanedPublishTargets,
27
27
  Check::PublishTreeIndexOverlap,
28
28
  Check::ProposalTargets,
@@ -33,7 +33,7 @@ module Textus
33
33
 
34
34
  module_function
35
35
 
36
- def build(container:, checks: nil)
36
+ def build(container:, checks: nil, role: Textus::Role::DEFAULT)
37
37
  selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
38
38
  unknown = selected_keys - ALL_CHECKS
39
39
  unless unknown.empty?
@@ -43,7 +43,7 @@ module Textus
43
43
  end
44
44
 
45
45
  selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
46
- issues = selected.flat_map { |c| c.new(container).call }
46
+ issues = selected.flat_map { |c| c.new(container, role:).call }
47
47
  issues.concat(run_registered_checks(container))
48
48
 
49
49
  summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
@@ -56,12 +56,12 @@ module Textus
56
56
  end
57
57
 
58
58
  def run_registered_checks(container)
59
- container.rpc.names(:validate).flat_map { |name| invoke_registered_check(container, name) }
59
+ container.steps.names(:validate).flat_map { |name| invoke_registered_check(container, name) }
60
60
  end
61
61
 
62
62
  def invoke_registered_check(container, name)
63
63
  result = Timeout.timeout(DOCTOR_CHECK_TIMEOUT_SECONDS) do
64
- container.rpc.invoke(:validate, name, caps: container)
64
+ container.steps.invoke(:validate, name, caps: container)
65
65
  end
66
66
  return result.map { |h| h.transform_keys(&:to_s) } if result.is_a?(Array)
67
67
 
@@ -75,7 +75,7 @@ module Textus
75
75
  rescue StandardError => e
76
76
  [fail_issue(name, code: "doctor_check.failed",
77
77
  message: "#{e.class}: #{e.message}",
78
- fix: "fix the :validate hook in .textus/hooks/")]
78
+ fix: "fix the :validate step in .textus/steps/validate/")]
79
79
  end
80
80
 
81
81
  def fail_issue(name, code:, message:, fix:)
@@ -32,42 +32,15 @@ module Textus
32
32
  end
33
33
 
34
34
  def put(key, mentry:, payload:, if_etag: nil)
35
- path = @manifest.resolver.resolve(key).path
36
-
37
- meta = payload.meta || {}
38
-
39
- existing_uid = @reader.existing_uid(key)
40
- meta, content = ensure_uid(mentry.format, meta, payload.content, existing_uid)
41
-
42
- bytes, eff_meta, eff_body, eff_content = serialize_for_put(
43
- mentry: mentry, path: path,
44
- meta: meta, body: payload.body, content: content
45
- )
46
-
35
+ path = resolve_path(key)
36
+ meta, content = prepare_uid(mentry, payload, key)
37
+ bytes, eff_meta, eff_body, eff_content = serialize_entry(mentry, path, meta, payload, content)
47
38
  enforce_name_match!(path, eff_meta, mentry.format)
48
-
49
- schema = @schemas.fetch_or_nil(mentry.schema)
50
- if schema
51
- Entry.for_format(mentry.format).validate_against(
52
- schema,
53
- { "_meta" => eff_meta, "content" => eff_content },
54
- )
55
- end
56
-
57
- etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
58
- raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
59
-
60
- @file_store.write(path, bytes)
61
- etag_after = Etag.for_bytes(bytes)
62
- envelope = Textus::Envelope.build(
63
- key: key, mentry: mentry, path: path,
64
- meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
65
- )
66
- @audit_log.append(
67
- role: @call.role, verb: "put", key: key,
68
- etag_before: etag_before, etag_after: etag_after,
69
- extras: @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
70
- )
39
+ validate_schema(mentry, eff_meta, eff_content)
40
+ etag_before = check_etag!(path, key, if_etag)
41
+ write_bytes(path, bytes)
42
+ envelope = build_envelope(key, mentry, path, eff_meta, eff_body, eff_content)
43
+ audit_put(key, etag_before, envelope.etag)
71
44
  envelope
72
45
  end
73
46
 
@@ -155,7 +128,7 @@ module Textus
155
128
  # The zone directory under which `path` lives (`<root>/zones/<zone>`),
156
129
  # or nil if `path` is not under the store's zones tree.
157
130
  def zone_floor(path)
158
- zones_root = File.join(@manifest.data.root, "zones")
131
+ zones_root = File.join(@manifest.data.root, "data")
159
132
  prefix = "#{zones_root}/"
160
133
  return nil unless path.start_with?(prefix)
161
134
 
@@ -176,6 +149,62 @@ module Textus
176
149
  meta: meta, body: body, content: content, path: path,
177
150
  )
178
151
  end
152
+
153
+ def resolve_path(key)
154
+ @manifest.resolver.resolve(key).path
155
+ end
156
+
157
+ def prepare_uid(mentry, payload, key)
158
+ meta = payload.meta || {}
159
+ existing_uid = @reader.existing_uid(key)
160
+ ensure_uid(mentry.format, meta, payload.content, existing_uid)
161
+ end
162
+
163
+ def serialize_entry(mentry, path, meta, payload, content)
164
+ serialize_for_put(
165
+ mentry: mentry, path: path,
166
+ meta: meta, body: payload.body, content: content
167
+ )
168
+ end
169
+
170
+ def validate_schema(mentry, eff_meta, eff_content)
171
+ schema = @schemas.fetch_or_nil(mentry.schema)
172
+ return unless schema
173
+
174
+ Entry.for_format(mentry.format).validate_against(
175
+ schema,
176
+ { "_meta" => eff_meta, "content" => eff_content },
177
+ )
178
+ end
179
+
180
+ def check_etag!(path, key, if_etag)
181
+ etag_before = @file_store.exists?(path) ? @file_store.etag(path) : nil
182
+ raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && (etag_before != if_etag)
183
+
184
+ etag_before
185
+ end
186
+
187
+ def write_bytes(path, bytes)
188
+ @file_store.write(path, bytes)
189
+ end
190
+
191
+ def build_envelope(key, mentry, path, eff_meta, eff_body, eff_content)
192
+ Textus::Envelope.build(
193
+ key: key, mentry: mentry, path: path,
194
+ meta: eff_meta, body: eff_body,
195
+ etag: Etag.for_bytes(@file_store.read(path)),
196
+ content: eff_content
197
+ )
198
+ end
199
+
200
+ def audit_put(key, etag_before, etag_after)
201
+ extras = @call.correlation_id ? { "correlation_id" => @call.correlation_id } : nil
202
+ @audit_log.append(
203
+ role: @call.role, verb: "put", key: key,
204
+ etag_before: etag_before, etag_after: etag_after,
205
+ extras: extras
206
+ )
207
+ end
179
208
  end
180
209
  end
181
210
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Textus
4
4
  Envelope = Data.define(
5
- :protocol, :key, :zone, :owner, :path, :format,
5
+ :protocol, :key, :lane, :owner, :path, :format,
6
6
  :uid, :etag, :schema_ref, :meta, :body, :content, :freshness
7
7
  ) do
8
8
  # rubocop:disable Metrics/ParameterLists
@@ -11,7 +11,7 @@ module Textus
11
11
  new(
12
12
  protocol: Textus::PROTOCOL,
13
13
  key: key,
14
- zone: mentry.zone,
14
+ lane: mentry.lane,
15
15
  owner: mentry.owner,
16
16
  path: path,
17
17
  format: mentry.format,
@@ -34,7 +34,7 @@ module Textus
34
34
  h = {
35
35
  "protocol" => protocol,
36
36
  "key" => key,
37
- "zone" => zone,
37
+ "lane" => lane,
38
38
  "owner" => owner,
39
39
  "path" => path,
40
40
  "format" => format,
@@ -49,6 +49,8 @@ module Textus
49
49
  h
50
50
  end
51
51
 
52
+ alias_method :zone, :lane
53
+
52
54
  def stale?
53
55
  return false if freshness.nil?
54
56
 
data/lib/textus/errors.rb CHANGED
@@ -1,24 +1,32 @@
1
1
  module Textus
2
+ ErrorInfo = Data.define(:details, :exit_code, :hint) do
3
+ def self.for(details: {}, exit_code: 1, hint: nil)
4
+ new(details: details, exit_code: exit_code, hint: hint)
5
+ end
6
+ end
7
+
2
8
  class Error < StandardError
3
- attr_reader :code, :details, :exit_code, :hint
9
+ attr_reader :code
4
10
 
5
- def initialize(code, message, details: {}, exit_code: 1, hint: nil)
11
+ def initialize(code, message, info: nil, details: {}, exit_code: 1, hint: nil)
6
12
  super(message)
7
13
  @code = code
8
- @details = details
9
- @exit_code = exit_code
10
- @hint = hint
14
+ @info = info || ErrorInfo.for(details:, exit_code:, hint:)
11
15
  end
12
16
 
17
+ def details = @info.details
18
+ def exit_code = @info.exit_code
19
+ def hint = @info.hint
20
+
13
21
  def to_envelope
14
22
  env = {
15
23
  "protocol" => Textus::PROTOCOL,
16
24
  "ok" => false,
17
25
  "code" => @code,
18
26
  "message" => message,
19
- "details" => @details,
27
+ "details" => details,
20
28
  }
21
- env["hint"] = @hint if @hint
29
+ env["hint"] = hint if hint
22
30
  env
23
31
  end
24
32
  end
@@ -97,7 +105,7 @@ module Textus
97
105
  else
98
106
  "no declared role"
99
107
  end
100
- details = { "key" => k, "zone" => z }
108
+ details = { "key" => k, "lane" => z }
101
109
  details["verb"] = verb if verb
102
110
  details["holders"] = holders if holders
103
111
  super(
@@ -166,9 +174,9 @@ module Textus
166
174
  def initialize(m, format: nil)
167
175
  hint =
168
176
  if format
169
- "the template rendered invalid #{format}; try rendering with mock data and parsing the output before re-running reconcile"
177
+ "the template rendered invalid #{format}; try rendering with mock data and parsing the output before re-running drain"
170
178
  else
171
- "the template rendered invalid content; try rendering with mock data and parsing the output before re-running reconcile"
179
+ "the template rendered invalid content; try rendering with mock data and parsing the output before re-running drain"
172
180
  end
173
181
  super("bad_render", m, hint: hint)
174
182
  end
@@ -177,7 +185,7 @@ module Textus
177
185
  class PublishError < Error
178
186
  def initialize(m, target: nil)
179
187
  hint =
180
- ("file at #{target} wasn't published by textus; back it up and delete it, or move it under .textus/zones/" if target)
188
+ ("file at #{target} wasn't published by textus; back it up and delete it, or move it under .textus/data/" if target)
181
189
  super("publish_error", m, details: target ? { "target" => target } : {}, hint: hint)
182
190
  end
183
191
  end
@@ -0,0 +1,21 @@
1
+ module Textus
2
+ module Events
3
+ ENTRY_WRITTEN = "entry.written"
4
+ ENTRY_DELETED = "entry.deleted"
5
+ ENTRY_RENAMED = "entry.renamed"
6
+ ENTRY_FETCHED = "entry.fetched"
7
+ ENTRY_DERIVED = "entry.derived"
8
+ ENTRY_VALIDATED = "entry.validated"
9
+ ENTRY_PUBLISHED = "entry.published"
10
+ PIPELINE_FAILED = "pipeline.failed"
11
+
12
+ STEP_FETCH_COMPLETE = "step.fetch.complete"
13
+ STEP_TRANSFORM_COMPLETE = "step.transform.complete"
14
+ STEP_VALIDATE_PASSED = "step.validate.passed"
15
+ STEP_VALIDATE_FAILED = "step.validate.failed"
16
+
17
+ STORE_LOADED = "store.loaded"
18
+ SESSION_OPENED = "session.opened"
19
+ SESSION_CLOSED = "session.closed"
20
+ end
21
+ end