textus 0.52.0 → 0.54.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 (261) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/README.md +62 -54
  4. data/SPEC.md +64 -192
  5. data/docs/architecture/README.md +157 -156
  6. data/exe/textus +1 -1
  7. data/lib/textus/action/accept.rb +44 -0
  8. data/lib/textus/action/audit.rb +131 -0
  9. data/lib/textus/action/base.rb +42 -0
  10. data/lib/textus/{read → action}/blame.rb +28 -22
  11. data/lib/textus/action/boot.rb +17 -0
  12. data/lib/textus/action/data_mv.rb +69 -0
  13. data/lib/textus/action/deps.rb +34 -0
  14. data/lib/textus/action/doctor.rb +24 -0
  15. data/lib/textus/action/drain.rb +39 -0
  16. data/lib/textus/action/enqueue.rb +53 -0
  17. data/lib/textus/action/get.rb +78 -0
  18. data/lib/textus/action/ingest.rb +135 -0
  19. data/lib/textus/action/jobs.rb +36 -0
  20. data/lib/textus/action/key_delete.rb +36 -0
  21. data/lib/textus/action/key_delete_prefix.rb +44 -0
  22. data/lib/textus/action/key_mv.rb +129 -0
  23. data/lib/textus/action/key_mv_prefix.rb +57 -0
  24. data/lib/textus/action/list.rb +42 -0
  25. data/lib/textus/action/propose.rb +52 -0
  26. data/lib/textus/action/published.rb +24 -0
  27. data/lib/textus/action/pulse.rb +56 -0
  28. data/lib/textus/action/put.rb +52 -0
  29. data/lib/textus/action/rdeps.rb +39 -0
  30. data/lib/textus/action/reject.rb +37 -0
  31. data/lib/textus/action/rule_explain.rb +91 -0
  32. data/lib/textus/action/rule_lint.rb +68 -0
  33. data/lib/textus/action/rule_list.rb +42 -0
  34. data/lib/textus/action/schema_envelope.rb +29 -0
  35. data/lib/textus/action/uid.rb +33 -0
  36. data/lib/textus/action/where.rb +36 -0
  37. data/lib/textus/action/write_verb.rb +44 -0
  38. data/lib/textus/boot.rb +64 -132
  39. data/lib/textus/command.rb +41 -0
  40. data/lib/textus/container.rb +1 -1
  41. data/lib/textus/{domain → core}/duration.rb +1 -1
  42. data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
  43. data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
  44. data/lib/textus/{domain → core}/freshness.rb +2 -2
  45. data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
  46. data/lib/textus/{domain → core}/retention.rb +2 -2
  47. data/lib/textus/{domain → core}/sentinel.rb +1 -1
  48. data/lib/textus/doctor/check/generator_drift.rb +1 -1
  49. data/lib/textus/doctor/check/illegal_keys.rb +1 -1
  50. data/lib/textus/doctor/check/notebook_sources.rb +50 -0
  51. data/lib/textus/doctor/check/proposal_targets.rb +3 -3
  52. data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
  53. data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
  54. data/lib/textus/doctor/check/schema_violations.rb +8 -2
  55. data/lib/textus/doctor/check.rb +11 -9
  56. data/lib/textus/{read → doctor}/validator.rb +22 -13
  57. data/lib/textus/doctor.rb +4 -44
  58. data/lib/textus/envelope/reader.rb +46 -0
  59. data/lib/textus/envelope/writer.rb +209 -0
  60. data/lib/textus/envelope.rb +5 -3
  61. data/lib/textus/errors.rb +25 -13
  62. data/lib/textus/{entry → format}/base.rb +1 -6
  63. data/lib/textus/{entry → format}/json.rb +1 -4
  64. data/lib/textus/{entry → format}/markdown.rb +2 -6
  65. data/lib/textus/{entry → format}/text.rb +2 -6
  66. data/lib/textus/{entry → format}/yaml.rb +1 -4
  67. data/lib/textus/{entry.rb → format.rb} +9 -10
  68. data/lib/textus/gate/auth.rb +227 -0
  69. data/lib/textus/gate.rb +116 -0
  70. data/lib/textus/init.rb +59 -105
  71. data/lib/textus/jobs/base.rb +23 -0
  72. data/lib/textus/jobs/materialize.rb +20 -0
  73. data/lib/textus/jobs/plan.rb +9 -0
  74. data/lib/textus/jobs/planner.rb +101 -0
  75. data/lib/textus/jobs/retention.rb +48 -0
  76. data/lib/textus/jobs/sweep.rb +27 -0
  77. data/lib/textus/jobs/worker.rb +67 -0
  78. data/lib/textus/jobs.rb +15 -0
  79. data/lib/textus/key/path.rb +10 -3
  80. data/lib/textus/layout.rb +13 -0
  81. data/lib/textus/manifest/data.rb +14 -14
  82. data/lib/textus/manifest/entry/base.rb +14 -25
  83. data/lib/textus/manifest/entry/parser.rb +31 -14
  84. data/lib/textus/manifest/entry/produced.rb +13 -33
  85. data/lib/textus/manifest/entry/publish/mode.rb +1 -1
  86. data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
  87. data/lib/textus/manifest/entry/publish.rb +0 -12
  88. data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
  89. data/lib/textus/manifest/entry/validators.rb +0 -1
  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 +3 -3
  94. data/lib/textus/manifest/policy/source.rb +30 -0
  95. data/lib/textus/manifest/policy.rb +38 -53
  96. data/lib/textus/manifest/resolver.rb +5 -4
  97. data/lib/textus/manifest/rules.rb +4 -4
  98. data/lib/textus/manifest/schema/keys.rb +20 -28
  99. data/lib/textus/manifest/schema/validator.rb +27 -33
  100. data/lib/textus/manifest/schema/vocabulary.rb +2 -1
  101. data/lib/textus/manifest/schema.rb +2 -2
  102. data/lib/textus/manifest.rb +2 -2
  103. data/lib/textus/ports/audit_log.rb +31 -5
  104. data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
  105. data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
  106. data/lib/textus/ports/sentinel_store.rb +2 -2
  107. data/lib/textus/ports/watcher_lock.rb +48 -0
  108. data/lib/textus/produce/engine.rb +25 -70
  109. data/lib/textus/produce/render.rb +2 -7
  110. data/lib/textus/schema/tools.rb +4 -3
  111. data/lib/textus/session.rb +6 -3
  112. data/lib/textus/store.rb +12 -19
  113. data/lib/textus/surfaces/cli/group/data.rb +11 -0
  114. data/lib/textus/surfaces/cli/group/key.rb +11 -0
  115. data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
  116. data/lib/textus/surfaces/cli/group/rule.rb +11 -0
  117. data/lib/textus/surfaces/cli/group/schema.rb +11 -0
  118. data/lib/textus/surfaces/cli/group.rb +50 -0
  119. data/lib/textus/surfaces/cli/runner.rb +236 -0
  120. data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
  121. data/lib/textus/surfaces/cli/verb/get.rb +21 -0
  122. data/lib/textus/surfaces/cli/verb/init.rb +20 -0
  123. data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
  124. data/lib/textus/surfaces/cli/verb/put.rb +30 -0
  125. data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
  126. data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
  127. data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
  128. data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
  129. data/lib/textus/surfaces/cli/verb.rb +111 -0
  130. data/lib/textus/surfaces/cli.rb +148 -0
  131. data/lib/textus/surfaces/mcp/catalog.rb +99 -0
  132. data/lib/textus/surfaces/mcp/errors.rb +25 -0
  133. data/lib/textus/surfaces/mcp/routing.rb +51 -0
  134. data/lib/textus/surfaces/mcp/server.rb +184 -0
  135. data/lib/textus/surfaces/mcp/session.rb +9 -0
  136. data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
  137. data/lib/textus/surfaces/mcp.rb +8 -0
  138. data/lib/textus/surfaces/role_scope.rb +34 -0
  139. data/lib/textus/surfaces/watcher.rb +38 -0
  140. data/lib/textus/version.rb +1 -1
  141. data/lib/textus/workflow/collector.rb +27 -0
  142. data/lib/textus/workflow/context.rb +5 -0
  143. data/lib/textus/workflow/dsl.rb +44 -0
  144. data/lib/textus/workflow/loader.rb +17 -0
  145. data/lib/textus/workflow/pattern.rb +18 -0
  146. data/lib/textus/workflow/registry.rb +21 -0
  147. data/lib/textus/workflow/runner.rb +84 -0
  148. data/lib/textus/workflow.rb +28 -0
  149. data/lib/textus.rb +75 -22
  150. metadata +118 -132
  151. data/lib/textus/cli/group/hook.rb +0 -9
  152. data/lib/textus/cli/group/key.rb +0 -9
  153. data/lib/textus/cli/group/mcp.rb +0 -9
  154. data/lib/textus/cli/group/rule.rb +0 -9
  155. data/lib/textus/cli/group/schema.rb +0 -9
  156. data/lib/textus/cli/group/zone.rb +0 -9
  157. data/lib/textus/cli/group.rb +0 -48
  158. data/lib/textus/cli/runner.rb +0 -193
  159. data/lib/textus/cli/verb/doctor.rb +0 -17
  160. data/lib/textus/cli/verb/get.rb +0 -18
  161. data/lib/textus/cli/verb/hook_run.rb +0 -48
  162. data/lib/textus/cli/verb/hooks.rb +0 -50
  163. data/lib/textus/cli/verb/init.rb +0 -18
  164. data/lib/textus/cli/verb/mcp_serve.rb +0 -22
  165. data/lib/textus/cli/verb/put.rb +0 -30
  166. data/lib/textus/cli/verb/schema_diff.rb +0 -15
  167. data/lib/textus/cli/verb/schema_init.rb +0 -19
  168. data/lib/textus/cli/verb/schema_migrate.rb +0 -19
  169. data/lib/textus/cli/verb/serve.rb +0 -19
  170. data/lib/textus/cli/verb.rb +0 -116
  171. data/lib/textus/cli.rb +0 -138
  172. data/lib/textus/dispatcher.rb +0 -54
  173. data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
  174. data/lib/textus/doctor/check/hooks.rb +0 -32
  175. data/lib/textus/doctor/check/intake_registration.rb +0 -46
  176. data/lib/textus/domain/action.rb +0 -9
  177. data/lib/textus/domain/jobs/registry.rb +0 -37
  178. data/lib/textus/domain/permission.rb +0 -7
  179. data/lib/textus/domain/policy/base_guards.rb +0 -25
  180. data/lib/textus/domain/policy/evaluation.rb +0 -15
  181. data/lib/textus/domain/policy/guard.rb +0 -35
  182. data/lib/textus/domain/policy/guard_factory.rb +0 -40
  183. data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
  184. data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
  185. data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
  186. data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
  187. data/lib/textus/domain/policy/predicates/registry.rb +0 -39
  188. data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
  189. data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
  190. data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
  191. data/lib/textus/domain/policy/source.rb +0 -73
  192. data/lib/textus/envelope/io/reader.rb +0 -48
  193. data/lib/textus/envelope/io/writer.rb +0 -182
  194. data/lib/textus/hooks/builtin.rb +0 -70
  195. data/lib/textus/hooks/catalog.rb +0 -37
  196. data/lib/textus/hooks/context.rb +0 -72
  197. data/lib/textus/hooks/error_log.rb +0 -32
  198. data/lib/textus/hooks/event_bus.rb +0 -114
  199. data/lib/textus/hooks/fire_report.rb +0 -23
  200. data/lib/textus/hooks/loader.rb +0 -54
  201. data/lib/textus/hooks/rpc_registry.rb +0 -43
  202. data/lib/textus/hooks/signature.rb +0 -31
  203. data/lib/textus/init/templates/machine_intake.rb +0 -45
  204. data/lib/textus/init/templates/orientation_reducer.rb +0 -17
  205. data/lib/textus/jobs/handlers.rb +0 -62
  206. data/lib/textus/jobs/scheduler.rb +0 -36
  207. data/lib/textus/jobs/seeder.rb +0 -57
  208. data/lib/textus/maintenance/drain.rb +0 -42
  209. data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
  210. data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
  211. data/lib/textus/maintenance/retention/apply.rb +0 -52
  212. data/lib/textus/maintenance/rule_lint.rb +0 -66
  213. data/lib/textus/maintenance/serve.rb +0 -30
  214. data/lib/textus/maintenance/worker.rb +0 -74
  215. data/lib/textus/maintenance/zone_mv.rb +0 -64
  216. data/lib/textus/maintenance.rb +0 -15
  217. data/lib/textus/manifest/entry/validators/events.rb +0 -22
  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/audit_subscriber.rb +0 -42
  226. data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
  227. data/lib/textus/produce/acquire/handler.rb +0 -29
  228. data/lib/textus/produce/acquire/intake.rb +0 -130
  229. data/lib/textus/produce/acquire/projection.rb +0 -127
  230. data/lib/textus/produce/acquire/serializer/json.rb +0 -31
  231. data/lib/textus/produce/acquire/serializer/text.rb +0 -16
  232. data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
  233. data/lib/textus/produce/acquire/serializer.rb +0 -17
  234. data/lib/textus/produce/events.rb +0 -36
  235. data/lib/textus/projection.rb +0 -103
  236. data/lib/textus/read/audit.rb +0 -130
  237. data/lib/textus/read/boot.rb +0 -26
  238. data/lib/textus/read/capabilities.rb +0 -70
  239. data/lib/textus/read/deps.rb +0 -38
  240. data/lib/textus/read/doctor.rb +0 -27
  241. data/lib/textus/read/freshness.rb +0 -152
  242. data/lib/textus/read/get.rb +0 -73
  243. data/lib/textus/read/jobs.rb +0 -31
  244. data/lib/textus/read/list.rb +0 -24
  245. data/lib/textus/read/published.rb +0 -22
  246. data/lib/textus/read/pulse.rb +0 -98
  247. data/lib/textus/read/rdeps.rb +0 -39
  248. data/lib/textus/read/rule_explain.rb +0 -96
  249. data/lib/textus/read/rule_list.rb +0 -54
  250. data/lib/textus/read/schema_envelope.rb +0 -25
  251. data/lib/textus/read/uid.rb +0 -29
  252. data/lib/textus/read/validate_all.rb +0 -36
  253. data/lib/textus/read/where.rb +0 -24
  254. data/lib/textus/role_scope.rb +0 -78
  255. data/lib/textus/write/accept.rb +0 -58
  256. data/lib/textus/write/enqueue.rb +0 -50
  257. data/lib/textus/write/key_delete.rb +0 -65
  258. data/lib/textus/write/key_mv.rb +0 -141
  259. data/lib/textus/write/propose.rb +0 -54
  260. data/lib/textus/write/put.rb +0 -74
  261. data/lib/textus/write/reject.rb +0 -68
@@ -18,8 +18,6 @@ module Textus
18
18
  # the block groups the two but does not make exclusivity structural, so
19
19
  # this stays the one enforcement point (ADR 0052 D2).
20
20
  def self.resolve(entry)
21
- reject_removed_publish_each(entry)
22
-
23
21
  set = []
24
22
  set << "publish.to" unless Array(entry.publish_to).empty?
25
23
  set << "publish.tree" unless entry.publish_tree.nil?
@@ -33,16 +31,6 @@ module Textus
33
31
  mode_for(entry, set.first)
34
32
  end
35
33
 
36
- def self.reject_removed_publish_each(entry)
37
- return unless entry.raw["publish_each"]
38
-
39
- raise Textus::UsageError.new(
40
- "entry '#{entry.key}': publish_each was removed in 0.42.0 (ADR 0051) — " \
41
- "mirror the subtree with `publish: { tree: \"...\" }`.",
42
- )
43
- end
44
- private_class_method :reject_removed_publish_each
45
-
46
34
  def self.mode_for(entry, key)
47
35
  case key
48
36
  when "publish.to" then ToPaths.new(entry)
@@ -5,7 +5,7 @@ module Textus
5
5
  module FormatMatrix
6
6
  def self.call(entry, policy:) # rubocop:disable Lint/UnusedMethodArgument
7
7
  begin
8
- Textus::Entry.for_format(entry.format).validate_path_extension(entry.path, entry.nested?)
8
+ Textus::Format.for(entry.format).validate_path_extension(entry.path, entry.nested?)
9
9
  rescue UsageError => e
10
10
  raise UsageError.new("entry '#{entry.key}': #{e.message}")
11
11
  end
@@ -3,7 +3,6 @@ module Textus
3
3
  class Entry
4
4
  module Validators
5
5
  REGISTERED = [
6
- Events,
7
6
  Publish,
8
7
  Ignore,
9
8
  FormatMatrix,
@@ -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
@@ -0,0 +1,30 @@
1
+ module Textus
2
+ class Manifest
3
+ class Policy
4
+ # An entry's external-generator declaration. `source:` now means ONLY
5
+ # `from: external` — out-of-band runner; textus never invokes it, only
6
+ # detects drift (doctor generator_drift check).
7
+ # `from: fetch` and `from: derive` are removed; those concerns now live
8
+ # in workflow files under .textus/workflows/.
9
+ class Source
10
+ attr_reader :command, :sources
11
+
12
+ def initialize(raw)
13
+ from = raw["from"].to_s
14
+ unless from == "external"
15
+ raise Textus::BadManifest.new(
16
+ "from: #{from} is removed — use a workflow file under .textus/workflows/ instead",
17
+ )
18
+ end
19
+
20
+ @command = raw["command"] or
21
+ raise Textus::BadManifest.new("source (from: external) requires a `command:` field")
22
+ @sources = raw["sources"] || []
23
+ end
24
+
25
+ def external? = true
26
+ def kind = :external
27
+ end
28
+ end
29
+ end
30
+ 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,67 +41,52 @@ 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.
80
- def derived_entry?(key)
81
- return false if @data.entries.nil?
82
-
83
- entry = @data.entries.find { |e| e.key == key } or return false
84
- entry.derived?
65
+ def derived_entry?(_key)
66
+ false
85
67
  end
86
68
 
87
- # The single zone declaring kind: machine, or nil.
88
- def machine_zone
89
- @data.declared_zone_kinds.key(:machine)
69
+ # The single lane declaring kind: machine, or nil.
70
+ def machine_lane
71
+ @data.declared_lane_kinds.key(:machine)
90
72
  end
91
73
 
92
- # A zone is a proposal queue iff it declares kind: queue.
93
- def queue_zone?(zone_name)
94
- declared_kind(zone_name) == :queue
74
+ # A lane is a proposal queue iff it declares kind: queue.
75
+ def queue_lane?(lane_name)
76
+ declared_kind(lane_name) == :queue
95
77
  end
96
78
 
97
- # The zone a proposer role writes proposals into: the single zone that
79
+ # The lane a proposer role writes proposals into: the single lane that
98
80
  # 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)
81
+ # is no queue lane or the role cannot write it.
82
+ def propose_lane_for(role)
101
83
  return nil if role.nil?
102
84
 
103
- q = queue_zone
104
- return nil unless q && zone_writers(q).include?(role)
85
+ q = queue_lane
86
+ return nil unless q
87
+
88
+ q_verb = verb_for_lane(q)
89
+ return nil unless roles_with_capability(q_verb).include?(role)
105
90
 
106
91
  q
107
92
  end
@@ -53,8 +53,9 @@ module Textus
53
53
  else
54
54
  raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless nested_entry?(entry)
55
55
 
56
- primary_ext = Textus::Entry.for_format(entry.format).extensions.first
57
- path = File.join(@data.root, "zones", entry.path, *remaining) + primary_ext
56
+ primary_ext = Textus::Format.for(entry.format).extensions.first
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)))
@@ -110,7 +111,7 @@ module Textus
110
111
  end
111
112
 
112
113
  def nested_glob(format)
113
- Textus::Entry.for_format(format).nested_glob
114
+ Textus::Format.for(format).nested_glob
114
115
  end
115
116
  end
116
117
  end
@@ -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,28 +4,21 @@ 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
12
- source publish
13
- events ignore tracked
11
+ key path lane kind schema owner nested format
12
+ source publish tracked ignore
14
13
  ].freeze
15
14
  # ADR 0052: the typed publish block — `publish: { to: [...] }` (file
16
15
  # fan-out) xor `publish: { tree: "dir" }` (subtree mirror).
17
16
  PUBLISH_KEYS = %w[to tree].freeze
18
- # ADR 0093/0094: entry-level acquisition block. `from: project` sources
19
- # expose flat projection fields (select/pluck/sort_by/transform) directly
20
- # on the source block (ADR 0094). Render fields (template/inject_boot/
21
- # provenance) that were formerly on the source are retired — they live on
22
- # publish targets. The legacy `project:` free hash and `template`/
23
- # `inject_boot`/`provenance` fields are kept here so the schema walk can
24
- # still emit the migration hint rather than a bare "unknown key".
25
- SOURCE_KEYS = %w[
26
- from handler config template project command sources ttl inject_boot provenance
27
- select pluck sort_by transform
28
- ].freeze
17
+ # entry-level external-generator block — only `from: external` is valid.
18
+ # `from: fetch` and `from: derive` and their specific fields (handler,
19
+ # config, ttl, select, pluck, sort_by, transform) were removed in the
20
+ # workflow redesign.
21
+ SOURCE_KEYS = %w[from command sources].freeze
29
22
  # ADR 0093: rule-level GC slot. drop/archive only (refresh gone).
30
23
  RETENTION_KEYS = %w[ttl action].freeze
31
24
 
@@ -40,12 +33,11 @@ module Textus
40
33
  # (in_rule_list / in_rule_explain)
41
34
  #
42
35
  # 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)
36
+ # yaml_key manifest key used in a rule block
37
+ # policy_class the Manifest::Policy backing the field (nil = raw value)
46
38
  # validation :immediate (instantiate the policy at parse, surfacing
47
39
  # shape errors eagerly), :deferred (shape-check + carry
48
- # the raw Hash; guard predicates validate at GuardFactory
40
+ # the raw Hash; guard predicates validate at Dispatch::Auth
49
41
  # build time, ADR 0031), or :tagged (pass the raw Hash to a
50
42
  # tagged-union policy that dispatches on its discriminator
51
43
  # field, e.g. upkeep's on:)
@@ -61,13 +53,6 @@ module Textus
61
53
  # Key order here fixes the order of RULE_KEYS (after match), the slots,
62
54
  # the RuleSet members, and the doctor SLOTS.
63
55
  FIELD_REGISTRY = {
64
- handler_allowlist: {
65
- yaml_key: "intake_handler_allowlist",
66
- policy_class: Textus::Domain::Policy::HandlerAllowlist,
67
- validation: :immediate, sub_keys: nil, arg_key: :handlers,
68
- in_pick: true, in_ambiguity: true,
69
- in_rule_list: true, in_rule_explain: %i[detail]
70
- },
71
56
  guard: {
72
57
  yaml_key: "guard",
73
58
  policy_class: nil,
@@ -77,11 +62,18 @@ module Textus
77
62
  },
78
63
  retention: {
79
64
  yaml_key: "retention",
80
- policy_class: Textus::Domain::Policy::Retention,
65
+ policy_class: Textus::Manifest::Policy::Retention,
81
66
  validation: :tagged, sub_keys: RETENTION_KEYS, arg_key: nil,
82
67
  in_pick: true, in_ambiguity: true,
83
68
  in_rule_list: true, in_rule_explain: %i[lean detail]
84
69
  },
70
+ react: {
71
+ yaml_key: "react",
72
+ policy_class: Textus::Manifest::Policy::React,
73
+ validation: :immediate, sub_keys: nil, arg_key: :raw,
74
+ in_pick: true, in_ambiguity: true,
75
+ in_rule_list: true, in_rule_explain: %i[lean detail]
76
+ },
85
77
  }.freeze
86
78
 
87
79
  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
@@ -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
 
@@ -287,33 +289,25 @@ module Textus
287
289
  # out). Per ADR 0095 the produce-method is read from source.from on the one
288
290
  # Produced kind, so there is no longer a kind to agree against the source.
289
291
  # (Replaces validate_upkeep_kinds!.)
290
- def validate_source_and_retention!(manifest)
291
- manifest.data.entries.each do |entry|
292
- retention = manifest.rules.for(entry.key).retention
293
- next if retention.nil?
294
- next unless entry.derived?
295
-
296
- raise BadManifest.new(
297
- "entry '#{entry.key}': a derived entry regenerates from its source; " \
298
- "retention (drop/archive) is invalid",
299
- )
300
- end
292
+ def validate_source_and_retention!(_manifest)
293
+ # No-op: from: derive is removed; retention + produced entries are
294
+ # always valid (retention drives age-GC on workflow-produced entries).
301
295
  end
302
296
 
303
- # Write authority is derived from capabilities (ADR 0030): a zone of a
297
+ # Write authority is derived from capabilities (ADR 0030): a lane of a
304
298
  # 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
299
+ # verb. Reject a manifest declaring a lane whose required verb is held by
306
300
  # no role. Capabilities.resolve returns the defaults when `roles:` is nil,
307
301
  # so the capability union is all four verbs and every kind is satisfied.
308
- def validate_zone_kind_consistency!(raw)
302
+ def validate_lane_kind_consistency!(raw)
309
303
  held = Capabilities.resolve(raw["roles"]).values.flatten.uniq
310
304
 
311
- Array(raw["zones"]).each_with_index do |z, i|
305
+ Array(raw["lanes"]).each_with_index do |z, i|
312
306
  verb = KIND_REQUIRES_VERB[z["kind"]]
313
307
  next if verb.nil? || held.include?(verb)
314
308
 
315
309
  raise BadManifest.new(
316
- "zone '#{z["name"]}' (#{z["kind"]}) at '$.zones[#{i}]' " \
310
+ "lane '#{z["name"]}' (#{z["kind"]}) at '$.lanes[#{i}]' " \
317
311
  "needs a role with capability '#{verb}'; none declared",
318
312
  )
319
313
  end
@@ -13,9 +13,10 @@ module Textus
13
13
  "workspace" => "keep",
14
14
  "machine" => "converge",
15
15
  "queue" => "propose",
16
+ "raw" => "ingest",
16
17
  }.freeze
17
18
 
18
- ZONE_KINDS = LANES.keys.freeze
19
+ LANE_KINDS = LANES.keys.freeze
19
20
  CAPABILITIES = LANES.values.uniq.freeze
20
21
  KIND_REQUIRES_VERB = LANES
21
22
  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.
@@ -78,16 +78,42 @@ module Textus
78
78
  def verify_integrity
79
79
  return [] unless File.exist?(@path)
80
80
 
81
- out = []
82
- File.foreach(@path).with_index(1) do |line, lineno|
83
- violation = check_line(line.chomp, lineno)
84
- out << violation if violation
81
+ [].tap do |out|
82
+ iterate_with_prev_seq do |line, lineno, prev_seq|
83
+ check_line_integrity(line, lineno, prev_seq, out)
84
+ end
85
85
  end
86
- out
87
86
  end
88
87
 
89
88
  private
90
89
 
90
+ def iterate_with_prev_seq
91
+ prev_seq = nil
92
+ File.foreach(@path).with_index(1) do |line, lineno|
93
+ yield line.chomp, lineno, prev_seq
94
+ parsed = parse_row(line.chomp)
95
+ prev_seq = parsed["seq"] if parsed&.dig("seq").is_a?(Integer)
96
+ end
97
+ end
98
+
99
+ def check_line_integrity(line, lineno, prev_seq, out)
100
+ violation = check_line(line, lineno)
101
+ if violation
102
+ out << violation
103
+ return
104
+ end
105
+
106
+ parsed = parse_row(line)
107
+ return unless parsed && (seq = parsed["seq"]).is_a?(Integer)
108
+ return unless prev_seq && seq != prev_seq + 1
109
+
110
+ out << {
111
+ "lineno" => lineno,
112
+ "reason" => "seq_gap",
113
+ "detail" => "expected #{prev_seq + 1}, got #{seq}",
114
+ }
115
+ end
116
+
91
117
  def rotated(n)
92
118
  File.join(Textus::Layout.audit_dir(@root), "audit.log.#{n}")
93
119
  end