textus 0.18.0 → 0.20.2

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +43 -48
  3. data/CHANGELOG.md +238 -0
  4. data/SPEC.md +35 -2
  5. data/lib/textus/application/context.rb +20 -58
  6. data/lib/textus/application/policy/predicates/accept_authority_signed.rb +33 -0
  7. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  8. data/lib/textus/{domain → application}/policy/promotion.rb +18 -6
  9. data/lib/textus/application/projection.rb +91 -0
  10. data/lib/textus/application/reads/audit.rb +4 -4
  11. data/lib/textus/application/reads/blame.rb +9 -8
  12. data/lib/textus/application/reads/deps.rb +14 -3
  13. data/lib/textus/application/reads/freshness.rb +10 -8
  14. data/lib/textus/application/reads/get.rb +10 -8
  15. data/lib/textus/application/reads/get_or_refresh.rb +3 -3
  16. data/lib/textus/application/reads/list.rb +3 -3
  17. data/lib/textus/application/reads/policy_explain.rb +3 -3
  18. data/lib/textus/application/reads/published.rb +5 -3
  19. data/lib/textus/application/reads/rdeps.rb +15 -3
  20. data/lib/textus/application/reads/schema_envelope.rb +5 -4
  21. data/lib/textus/application/reads/stale.rb +3 -3
  22. data/lib/textus/application/reads/uid.rb +11 -3
  23. data/lib/textus/application/reads/validate_all.rb +10 -6
  24. data/lib/textus/application/reads/validator.rb +5 -3
  25. data/lib/textus/application/reads/where.rb +3 -3
  26. data/lib/textus/application/refresh/all.rb +15 -11
  27. data/lib/textus/application/refresh/orchestrator.rb +9 -8
  28. data/lib/textus/application/refresh/worker.rb +56 -32
  29. data/lib/textus/application/writes/accept.rb +43 -16
  30. data/lib/textus/application/writes/authority_gate.rb +26 -0
  31. data/lib/textus/application/writes/delete.rb +13 -10
  32. data/lib/textus/application/writes/envelope_io.rb +64 -4
  33. data/lib/textus/application/writes/materializer.rb +50 -0
  34. data/lib/textus/application/writes/mv.rb +57 -94
  35. data/lib/textus/application/writes/publish.rb +132 -26
  36. data/lib/textus/application/writes/put.rb +15 -14
  37. data/lib/textus/application/writes/reject.rb +25 -12
  38. data/lib/textus/builder/pipeline.rb +21 -15
  39. data/lib/textus/builder/renderer/json.rb +4 -1
  40. data/lib/textus/builder/renderer/markdown.rb +7 -1
  41. data/lib/textus/builder/renderer/yaml.rb +4 -1
  42. data/lib/textus/cli/verb/build.rb +4 -6
  43. data/lib/textus/cli/verb/get.rb +1 -1
  44. data/lib/textus/cli/verb/hook_run.rb +3 -4
  45. data/lib/textus/cli/verb/hooks.rb +5 -5
  46. data/lib/textus/cli/verb/put.rb +2 -3
  47. data/lib/textus/cli/verb/refresh_stale.rb +1 -2
  48. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  49. data/lib/textus/doctor/check/hooks.rb +2 -2
  50. data/lib/textus/doctor/check/illegal_keys.rb +7 -7
  51. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  52. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  53. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  54. data/lib/textus/doctor/check/templates.rb +4 -3
  55. data/lib/textus/doctor.rb +3 -4
  56. data/lib/textus/domain/authorizer.rb +37 -0
  57. data/lib/textus/domain/policy/promote.rb +4 -2
  58. data/lib/textus/domain/policy/refresh.rb +2 -0
  59. data/lib/textus/domain/staleness/generator_check.rb +8 -7
  60. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  61. data/lib/textus/hooks/builtin.rb +6 -6
  62. data/lib/textus/hooks/bus.rb +155 -0
  63. data/lib/textus/hooks/context.rb +38 -0
  64. data/lib/textus/hooks/fire_report.rb +23 -0
  65. data/lib/textus/hooks/loader.rb +3 -3
  66. data/lib/textus/infra/audit_subscriber.rb +4 -4
  67. data/lib/textus/infra/event_bus.rb +3 -3
  68. data/lib/textus/infra/refresh/detached.rb +1 -1
  69. data/lib/textus/init.rb +3 -2
  70. data/lib/textus/intro.rb +51 -27
  71. data/lib/textus/manifest/entry/base.rb +38 -0
  72. data/lib/textus/manifest/entry/derived.rb +25 -0
  73. data/lib/textus/manifest/entry/intake.rb +19 -0
  74. data/lib/textus/manifest/entry/leaf.rb +16 -0
  75. data/lib/textus/manifest/entry/nested.rb +39 -0
  76. data/lib/textus/manifest/entry/parser.rb +58 -31
  77. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  78. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  79. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  80. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  81. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  82. data/lib/textus/manifest/entry.rb +0 -72
  83. data/lib/textus/manifest/resolver.rb +112 -0
  84. data/lib/textus/manifest/role_kinds.rb +21 -0
  85. data/lib/textus/manifest/schema.rb +46 -2
  86. data/lib/textus/manifest.rb +24 -101
  87. data/lib/textus/operations.rb +131 -74
  88. data/lib/textus/schema/tools.rb +10 -3
  89. data/lib/textus/store.rb +6 -6
  90. data/lib/textus/version.rb +1 -1
  91. metadata +18 -14
  92. data/lib/textus/application/writes/build.rb +0 -78
  93. data/lib/textus/cli/verb/key_normalize.rb +0 -19
  94. data/lib/textus/dependencies.rb +0 -23
  95. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  96. data/lib/textus/domain/policy.rb +0 -7
  97. data/lib/textus/hooks/dispatcher.rb +0 -71
  98. data/lib/textus/hooks/registry.rb +0 -85
  99. data/lib/textus/manifest/resolution.rb +0 -5
  100. data/lib/textus/migrate_keys.rb +0 -187
  101. data/lib/textus/projection.rb +0 -89
  102. data/lib/textus/refresh.rb +0 -39
@@ -1,37 +1,102 @@
1
1
  module Textus
2
2
  module Application
3
3
  module Writes
4
- # Copies nested-leaf entries to their `publish_each:` targets. Fires
5
- # `:file_published` for each copy. Mirror of `Build` for the publish
6
- # half split out from the old Build per ADR 0007.
4
+ # Single-pass publish use case: materializes Derived entries (template +
5
+ # projection + external runner) AND copies Leaf/Nested entries to their
6
+ # publish targets. Replaces the former two-step Build + Publish split.
7
+ #
8
+ # Return shape: { "protocol", "built", "published_leaves" }
9
+ # — wire-compatible with what the `textus build` CLI verb previously
10
+ # assembled by merging Build + old Publish results.
7
11
  class Publish
8
- def initialize(ctx:)
9
- @ctx = ctx
12
+ def initialize(ctx:, manifest:, file_store:, bus:, root:, store:, hook_context:) # rubocop:disable Metrics/ParameterLists
13
+ @ctx = ctx
14
+ @manifest = manifest
15
+ @file_store = file_store
16
+ @bus = bus
17
+ @root = root
18
+ @store = store
19
+ @hook_context = hook_context
10
20
  end
11
21
 
12
22
  def call(prefix: nil)
13
- repo_root = File.dirname(store.root)
14
- out = []
15
- manifest.entries.each do |mentry|
16
- next unless mentry.nested && mentry.publish_each
17
- next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
23
+ built = []
24
+ leaves = []
25
+ repo_root = File.dirname(@root)
18
26
 
19
- manifest.enumerate(prefix: mentry.key).each do |row|
20
- next unless row[:manifest_entry].equal?(mentry)
21
- next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
27
+ @manifest.entries.each do |mentry|
28
+ next if prefix && !entry_matches_prefix?(mentry, prefix)
22
29
 
23
- out << publish_leaf(mentry, row, repo_root)
30
+ case mentry
31
+ when Textus::Manifest::Entry::Derived
32
+ next unless mentry.in_generator_zone?
33
+
34
+ result = materialize_derived(mentry, repo_root)
35
+ built << result if result
36
+ when Textus::Manifest::Entry::Nested
37
+ next unless mentry.publish_each
38
+
39
+ publish_nested(mentry, repo_root, prefix, leaves)
40
+ when Textus::Manifest::Entry::Leaf
41
+ next if Array(mentry.publish_to).empty?
42
+
43
+ result = publish_leaf_entry(mentry, repo_root)
44
+ built << result if result
24
45
  end
25
46
  end
26
- { "protocol" => Textus::PROTOCOL, "published_leaves" => out }
47
+
48
+ { "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => leaves }
27
49
  end
28
50
 
29
51
  private
30
52
 
31
- def store = @ctx.store
32
- def manifest = store.manifest
53
+ # Materialize a Derived entry and copy to publish_to targets.
54
+ def materialize_derived(mentry, repo_root)
55
+ target_path = Materializer.new(
56
+ ctx: @ctx, manifest: @manifest, file_store: @file_store,
57
+ bus: @bus, root: @root, store: @store
58
+ ).run(mentry)
33
59
 
34
- def publish_leaf(mentry, row, repo_root)
60
+ publish_derived_copies(mentry, target_path, repo_root)
61
+ fire_build_completed(mentry)
62
+
63
+ { "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
64
+ end
65
+
66
+ def publish_derived_copies(mentry, target_path, repo_root)
67
+ envelope = reader.call(mentry.key)
68
+ mentry.publish_to.each do |rel|
69
+ target_abs = File.join(repo_root, rel)
70
+ Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: @root)
71
+ publish_event(:file_published,
72
+ key: mentry.key,
73
+ envelope: envelope,
74
+ source: target_path,
75
+ target: target_abs)
76
+ end
77
+ end
78
+
79
+ def fire_build_completed(mentry)
80
+ envelope = reader.call(mentry.key)
81
+ src = mentry.source
82
+ selects = src.is_a?(Textus::Manifest::Entry::Derived::Projection) ? Array(src.select).compact : []
83
+ publish_event(:build_completed,
84
+ key: mentry.key,
85
+ envelope: envelope,
86
+ sources: selects)
87
+ end
88
+
89
+ # Publish each leaf under a Nested entry's publish_each pattern.
90
+ def publish_nested(mentry, repo_root, prefix, accumulator)
91
+ @manifest.resolver.enumerate(prefix: mentry.key).each do |row|
92
+ next unless row[:manifest_entry].equal?(mentry)
93
+ next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
94
+
95
+ accumulator << publish_nested_leaf(mentry, row, repo_root)
96
+ end
97
+ end
98
+
99
+ def publish_nested_leaf(mentry, row, repo_root)
35
100
  target_rel = mentry.publish_target_for(row[:key])
36
101
  target_abs = File.expand_path(File.join(repo_root, target_rel))
37
102
  unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
@@ -40,16 +105,57 @@ module Textus
40
105
  )
41
106
  end
42
107
 
43
- Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: store.root)
44
- @ctx.bus.publish(:file_published,
45
- store: @ctx.with_role(@ctx.role),
46
- key: row[:key],
47
- envelope: Textus::Application::Reads::Get.new(ctx: @ctx).call(row[:key]),
48
- source: row[:path],
49
- target: target_abs,
50
- correlation_id: @ctx.correlation_id)
108
+ Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
109
+ publish_event(:file_published,
110
+ key: row[:key],
111
+ envelope: reader.call(row[:key]),
112
+ source: row[:path],
113
+ target: target_abs)
51
114
  { "key" => row[:key], "source" => row[:path], "target" => target_abs }
52
115
  end
116
+
117
+ # Publish a standalone Leaf entry that has publish_to targets.
118
+ def publish_leaf_entry(mentry, repo_root)
119
+ source_path = @manifest.resolver.resolve(mentry.key).path
120
+ envelope = reader.call(mentry.key)
121
+
122
+ mentry.publish_to.each do |rel|
123
+ target_abs = File.join(repo_root, rel)
124
+ Textus::Infra::Publisher.publish(source: source_path, target: target_abs, store_root: @root)
125
+ publish_event(:file_published,
126
+ key: mentry.key,
127
+ envelope: envelope,
128
+ source: source_path,
129
+ target: target_abs)
130
+ end
131
+
132
+ { "key" => mentry.key, "path" => source_path, "published_to" => mentry.publish_to }
133
+ end
134
+
135
+ # Whether the entry should be processed for the given prefix filter.
136
+ def entry_matches_prefix?(mentry, prefix)
137
+ return true unless prefix
138
+
139
+ case mentry
140
+ when Textus::Manifest::Entry::Nested
141
+ # Nested: process if the entry key is a prefix of `prefix` or
142
+ # `prefix` is a prefix of the entry key (a leaf under it).
143
+ mentry.key.start_with?(prefix) ||
144
+ prefix.start_with?("#{mentry.key}.")
145
+ else
146
+ mentry.key.start_with?(prefix)
147
+ end
148
+ end
149
+
150
+ def reader
151
+ @reader ||= Textus::Application::Reads::Get.new(
152
+ ctx: @ctx, manifest: @manifest, file_store: @file_store,
153
+ )
154
+ end
155
+
156
+ def publish_event(event, **payload)
157
+ @bus.publish(event, ctx: @hook_context, **payload)
158
+ end
53
159
  end
54
160
  end
55
161
  end
@@ -2,16 +2,20 @@ module Textus
2
2
  module Application
3
3
  module Writes
4
4
  class Put
5
- def initialize(ctx:, envelope_io:)
6
- @ctx = ctx
7
- @envelope_io = envelope_io
5
+ def initialize(ctx:, manifest:, envelope_io:, bus:, authorizer:, hook_context:)
6
+ @ctx = ctx
7
+ @manifest = manifest
8
+ @envelope_io = envelope_io
9
+ @bus = bus
10
+ @authorizer = authorizer
11
+ @hook_context = hook_context
8
12
  end
9
13
 
10
- def call(key, meta: nil, body: nil, content: nil, if_etag: nil, suppress_events: false)
11
- @ctx.manifest.validate_key!(key)
12
- mentry = @ctx.manifest.resolve(key).entry
14
+ def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
15
+ @manifest.validate_key!(key)
16
+ mentry = @manifest.resolver.resolve(key).entry
13
17
 
14
- @ctx.authorize_write!(mentry)
18
+ @authorizer.authorize_write!(mentry, role: @ctx.role)
15
19
 
16
20
  envelope = @envelope_io.write(
17
21
  key,
@@ -20,13 +24,10 @@ module Textus
20
24
  if_etag: if_etag,
21
25
  )
22
26
 
23
- unless suppress_events
24
- @ctx.bus.publish(:entry_put,
25
- store: @ctx.with_role(@ctx.role),
26
- key: key,
27
- envelope: envelope,
28
- correlation_id: @ctx.correlation_id)
29
- end
27
+ @bus.publish(:entry_put,
28
+ ctx: @hook_context,
29
+ key: key,
30
+ envelope: envelope)
30
31
 
31
32
  envelope
32
33
  end
@@ -1,33 +1,46 @@
1
+ require_relative "authority_gate"
2
+
1
3
  module Textus
2
4
  module Application
3
5
  module Writes
4
6
  class Reject
5
- def initialize(ctx:, envelope_io:)
6
- @ctx = ctx
7
- @envelope_io = envelope_io
7
+ include AuthorityGate
8
+
9
+ def initialize(ctx:, manifest:, file_store:, envelope_io:, bus:, authorizer:, hook_context:) # rubocop:disable Metrics/ParameterLists
10
+ @ctx = ctx
11
+ @manifest = manifest
12
+ @file_store = file_store
13
+ @envelope_io = envelope_io
14
+ @bus = bus
15
+ @authorizer = authorizer
16
+ @hook_context = hook_context
8
17
  end
9
18
 
10
19
  def call(pending_key)
11
- raise ProposalError.new("only human role can reject proposals; got '#{@ctx.role}'") unless @ctx.role == "human"
20
+ assert_accept_authority!("reject")
12
21
 
13
- mentry = @ctx.manifest.resolve(pending_key).entry
22
+ mentry = @manifest.resolver.resolve(pending_key).entry
14
23
  unless mentry.in_proposal_zone?
15
24
  raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})")
16
25
  end
17
26
 
18
- env = Textus::Application::Reads::Get.new(ctx: @ctx).call(pending_key)
27
+ env = Textus::Application::Reads::Get.new(
28
+ ctx: @ctx, manifest: @manifest, file_store: @file_store,
29
+ ).call(pending_key)
19
30
  proposal = env.meta&.dig("proposal") or
20
31
  raise ProposalError.new("entry has no proposal block: #{pending_key}")
21
32
  target_key = proposal["target_key"] or
22
33
  raise ProposalError.new("proposal missing target_key")
23
34
 
24
- Textus::Application::Writes::Delete.new(ctx: @ctx, envelope_io: @envelope_io).call(pending_key, suppress_events: true)
35
+ Textus::Application::Writes::Delete.new(
36
+ ctx: @ctx, manifest: @manifest, envelope_io: @envelope_io,
37
+ bus: @bus, authorizer: @authorizer, hook_context: @hook_context
38
+ ).call(pending_key, suppress_events: true)
25
39
 
26
- @ctx.bus.publish(:proposal_rejected,
27
- store: @ctx.with_role(@ctx.role),
28
- key: pending_key,
29
- target_key: target_key,
30
- correlation_id: @ctx.correlation_id)
40
+ @bus.publish(:proposal_rejected,
41
+ ctx: @hook_context,
42
+ key: pending_key,
43
+ target_key: target_key)
31
44
 
32
45
  { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
33
46
  end
@@ -7,11 +7,15 @@ module Textus
7
7
  # Returns a new hash with _meta as the first key, per SPEC §6 ordering.
8
8
  def self.call(content_hash, mentry)
9
9
  meta = { "generated_at" => Time.now.utc.iso8601 }
10
- from = Array(mentry.projection&.fetch("select", nil)).compact
11
- meta["from"] = from unless from.empty?
10
+ if mentry.is_a?(Textus::Manifest::Entry::Derived)
11
+ src = mentry.source
12
+ if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
13
+ from = Array(src.select).compact
14
+ meta["from"] = from unless from.empty?
15
+ meta["reduce"] = src.transform if src.transform
16
+ end
17
+ end
12
18
  meta["template"] = mentry.template if mentry.template
13
- reduce = mentry.projection&.dig("transform")
14
- meta["reduce"] = reduce if reduce
15
19
 
16
20
  out = { "_meta" => meta }
17
21
  content_hash.each { |k, v| out[k] = v unless k == "_meta" }
@@ -58,22 +62,23 @@ module Textus
58
62
  }
59
63
  end
60
64
 
61
- def self.run(store:, mentry:, template_loader:)
65
+ # rubocop:disable Metrics/ParameterLists
66
+ def self.run(mentry:, manifest:, reader:, lister:, transform_resolver:, template_loader:,
67
+ transform_context: nil, inject_intro: nil)
62
68
  # 1. Load sources + project + reduce
63
69
  data =
64
- if mentry.projection
65
- ops = Operations.for(store)
66
- Projection.new(
67
- reader: ops.method(:get),
68
- spec: mentry.projection,
69
- lister: ops.method(:list),
70
- transform_resolver: ->(name) { store.registry.rpc_callable(:transform_rows, name) },
71
- transform_context: Application::Context.system(store),
70
+ if mentry.is_a?(Textus::Manifest::Entry::Derived) && mentry.projection?
71
+ Application::Projection.new(
72
+ reader: reader,
73
+ spec: mentry.source.to_h.transform_keys(&:to_s),
74
+ lister: lister,
75
+ transform_resolver: transform_resolver,
76
+ transform_context: transform_context,
72
77
  ).run
73
78
  else
74
79
  { "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
75
80
  end
76
- data = data.merge("intro" => Intro.run(store)) if mentry.inject_intro
81
+ data = data.merge("intro" => inject_intro.call) if mentry.inject_intro && inject_intro
77
82
 
78
83
  # 2. Render
79
84
  klass = renderers[mentry.format] or
@@ -81,12 +86,13 @@ module Textus
81
86
  bytes = klass.new(template_loader: template_loader).call(mentry: mentry, data: data)
82
87
 
83
88
  # 3. Write (idempotent: skip if only generated_at would differ)
84
- target_path = Key::Path.resolve(store.manifest, mentry)
89
+ target_path = Key::Path.resolve(manifest, mentry)
85
90
  FileUtils.mkdir_p(File.dirname(target_path))
86
91
  write_if_changed(target_path, bytes, mentry.format)
87
92
 
88
93
  target_path
89
94
  end
95
+ # rubocop:enable Metrics/ParameterLists
90
96
 
91
97
  def self.write_if_changed(target_path, bytes, format)
92
98
  if File.exist?(target_path)
@@ -28,7 +28,10 @@ module Textus
28
28
  end
29
29
 
30
30
  def default_shape(mentry, data)
31
- if mentry.projection && mentry.projection["transform"] && data.is_a?(Hash) && !data.key?("entries")
31
+ has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
32
+ mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
33
+ mentry.source.transform
34
+ if has_transform && data.is_a?(Hash) && !data.key?("entries")
32
35
  data
33
36
  elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
34
37
  { "entries" => data["entries"] }
@@ -8,10 +8,16 @@ module Textus
8
8
  raise TemplateError.new("entry '#{mentry.key}': markdown build requires a template") unless mentry.template
9
9
 
10
10
  body = Mustache.render(@template_loader.call(mentry.template), data)
11
+ from = if mentry.is_a?(Textus::Manifest::Entry::Derived) &&
12
+ mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection)
13
+ Array(mentry.source.select).compact
14
+ else
15
+ []
16
+ end
11
17
  frontmatter = {
12
18
  "generated" => {
13
19
  "at" => Time.now.utc.iso8601,
14
- "from" => Array(mentry.projection&.fetch("select", nil)).compact,
20
+ "from" => from,
15
21
  },
16
22
  }
17
23
  Entry.for_format("markdown").serialize(meta: frontmatter, body: body)
@@ -28,7 +28,10 @@ module Textus
28
28
  end
29
29
 
30
30
  def default_shape(mentry, data)
31
- if mentry.projection && mentry.projection["transform"] && data.is_a?(Hash) && !data.key?("entries")
31
+ has_transform = mentry.is_a?(Textus::Manifest::Entry::Derived) &&
32
+ mentry.source.is_a?(Textus::Manifest::Entry::Derived::Projection) &&
33
+ mentry.source.transform
34
+ if has_transform && data.is_a?(Hash) && !data.key?("entries")
32
35
  data
33
36
  elsif data.is_a?(Hash) && data["entries"].is_a?(Array)
34
37
  { "entries" => data["entries"] }
@@ -8,12 +8,10 @@ module Textus
8
8
 
9
9
  def call(store)
10
10
  Textus::Infra::BuildLock.with(root: store.root) do
11
- ops = Textus::Operations.for(store, role: "builder")
12
- build_res = ops.build(prefix: prefix)
13
- publish_res = ops.publish(prefix: prefix)
14
- emit({ "protocol" => Textus::PROTOCOL,
15
- "built" => build_res["built"],
16
- "published_leaves" => publish_res["published_leaves"] })
11
+ role = store.manifest.roles_with_kind(:generator).first || "builder"
12
+ ops = Textus::Operations.for(store, role: role)
13
+ result = ops.publish(prefix: prefix)
14
+ emit(result)
17
15
  end
18
16
  end
19
17
  end
@@ -9,7 +9,7 @@ module Textus
9
9
  def call(store)
10
10
  key = positional.shift or raise UsageError.new("get requires a key")
11
11
  result = operations_for(store).get_or_refresh(key)
12
- raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
12
+ raise Textus::UnknownKey.new(key, suggestions: store.manifest.resolver.suggestions_for(key)) if result.nil?
13
13
 
14
14
  emit(result.to_h_for_wire)
15
15
  end
@@ -26,13 +26,12 @@ module Textus
26
26
  end
27
27
  end
28
28
 
29
- role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
30
- callable = store.registry.rpc_callable(:resolve_intake, name)
31
- view = Application::Context.new(store: store, role: role)
29
+ Role.resolve(flag: as_flag, env: ENV, root: store.root)
30
+ callable = store.bus.rpc_callable(:resolve_intake, name)
32
31
 
33
32
  begin
34
33
  Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
35
- callable.call(config: {}, store: view, args: args)
34
+ callable.call(config: {}, store: store, args: args)
36
35
  end
37
36
  rescue Timeout::Error
38
37
  raise UsageError.new(
@@ -7,7 +7,7 @@ module Textus
7
7
 
8
8
  option :event_filter, "--event=E"
9
9
 
10
- def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
10
+ def call(store) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
11
11
  subcommand = positional.first
12
12
  if subcommand
13
13
  raise UsageError.new("hook requires 'list'") unless subcommand == "list"
@@ -16,15 +16,15 @@ module Textus
16
16
  end
17
17
 
18
18
  rows = []
19
- Textus::Hooks::Registry::EVENTS.each do |event, spec|
19
+ Textus::Hooks::Bus::EVENTS.each do |event, spec|
20
20
  mode = spec[:mode].to_s
21
21
  case spec[:mode]
22
22
  when :rpc
23
- store.registry.rpc_names(event).each do |name|
23
+ store.bus.rpc_names(event).each do |name|
24
24
  rows << { "event" => event.to_s, "mode" => mode, "name" => name.to_s }
25
25
  end
26
26
  when :pubsub
27
- store.registry.pubsub_handlers(event).each do |h|
27
+ store.bus.pubsub_handlers(event).each do |h|
28
28
  row = { "event" => event.to_s, "mode" => mode, "name" => h[:name].to_s }
29
29
  row["keys"] = Array(h[:keys]) if h[:keys]
30
30
  rows << row
@@ -32,7 +32,7 @@ module Textus
32
32
  end
33
33
  end
34
34
  store.manifest.entries.each do |e|
35
- e.events.each do |evt, defs|
35
+ (e.respond_to?(:events) ? e.events : {}).each do |evt, defs|
36
36
  Array(defs).each do |defn|
37
37
  next unless defn["exec"]
38
38
 
@@ -17,12 +17,11 @@ module Textus
17
17
  raw = @stdin.read
18
18
  payload =
19
19
  if fetch_name
20
- callable = store.registry.rpc_callable(:resolve_intake, fetch_name)
20
+ callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
21
21
  result =
22
22
  begin
23
23
  Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
24
- callable.call(config: { "bytes" => raw },
25
- store: Textus::Application::Context.new(store: store, role: role), args: {})
24
+ callable.call(config: { "bytes" => raw }, store: store, args: {})
26
25
  end
27
26
  rescue Timeout::Error
28
27
  raise UsageError.new(
@@ -10,8 +10,7 @@ module Textus
10
10
  option :as_flag, "--as=ROLE"
11
11
 
12
12
  def call(store)
13
- ctx = context_for(store)
14
- result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
13
+ result = operations_for(store).refresh_all(prefix: prefix, zone: zone)
15
14
  emit(result)
16
15
  result["ok"] ? 0 : 1
17
16
  end
@@ -8,8 +8,9 @@ module Textus
8
8
  def call
9
9
  out = []
10
10
  store.manifest.entries.each do |mentry|
11
- handler = mentry.intake_handler
12
- next if handler.nil?
11
+ next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
12
+
13
+ handler = mentry.handler
13
14
 
14
15
  allow = store.manifest.rules_for(mentry.key).handler_allowlist
15
16
  next if allow.nil?
@@ -8,11 +8,11 @@ module Textus
8
8
  return out unless File.directory?(dir)
9
9
 
10
10
  Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
11
- registry = Textus::Hooks::Registry.new
11
+ bus = Textus::Hooks::Bus.new
12
12
  Textus.drain_hook_blocks
13
13
  begin
14
14
  load(f)
15
- Textus.drain_hook_blocks.each { |b| b.call(registry) }
15
+ Textus.drain_hook_blocks.each { |b| b.call(bus) }
16
16
  end
17
17
  rescue StandardError, ScriptError => e
18
18
  out << {
@@ -5,12 +5,13 @@ module Textus
5
5
  def call
6
6
  out = []
7
7
  store.manifest.entries.each do |entry|
8
- next unless entry.nested
8
+ next unless entry.nested?
9
9
 
10
10
  base = File.join(store.root, "zones", entry.path)
11
11
  next unless File.directory?(base)
12
12
 
13
- entry.index_filename ? check_index_paths(entry, base, out) : check_all_paths(base, out)
13
+ index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
14
+ index_fn ? check_index_paths(entry, index_fn, base, out) : check_all_paths(base, out)
14
15
  end
15
16
  out
16
17
  end
@@ -31,8 +32,8 @@ module Textus
31
32
  # segments leading to each index file participate in keys. Sibling
32
33
  # files and unrelated subtrees are not enumerated and must not be
33
34
  # flagged. Each illegal segment is reported once per path.
34
- def check_index_paths(entry, base, out)
35
- Dir.glob(File.join(base, "**", entry.index_filename)).each do |fp|
35
+ def check_index_paths(_entry, index_fn, base, out)
36
+ Dir.glob(File.join(base, "**", index_fn)).each do |fp|
36
37
  rel = fp.sub(%r{\A#{Regexp.escape(base)}/?}, "")
37
38
  File.dirname(rel).split("/").reject { |s| s.empty? || s == "." }.each do |seg|
38
39
  next if seg.match?(Key::Grammar::SEGMENT)
@@ -43,15 +44,14 @@ module Textus
43
44
  end
44
45
 
45
46
  def issue(abs_path, stem)
46
- proposed = Textus::MigrateKeys.normalize(stem)
47
47
  {
48
48
  "code" => "key.illegal",
49
49
  "level" => "error",
50
50
  "subject" => abs_path,
51
51
  "path" => abs_path,
52
- "proposed_key" => proposed,
53
52
  "message" => "illegal key segment '#{stem}' at #{abs_path}",
54
- "fix" => "run 'textus key normalize --dry-run' then '--write' to rename to '#{proposed}'",
53
+ "fix" => "rename the file/directory so each segment matches [a-z0-9][a-z0-9-]* " \
54
+ "(lowercase, digits, hyphens)",
55
55
  }
56
56
  end
57
57
 
@@ -6,7 +6,7 @@ module Textus
6
6
 
7
7
  def call
8
8
  declared = collect_declared_handlers
9
- registered = store.registry.rpc_names(:resolve_intake).to_set
9
+ registered = store.bus.rpc_names(:resolve_intake).to_set
10
10
 
11
11
  out = (declared - registered).map do |name|
12
12
  {
@@ -36,7 +36,7 @@ module Textus
36
36
  def collect_declared_handlers
37
37
  set = Set.new
38
38
  store.manifest.entries.each do |mentry|
39
- set << mentry.intake_handler.to_sym if mentry.intake_handler
39
+ set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
40
40
  end
41
41
  set
42
42
  end
@@ -4,7 +4,7 @@ module Textus
4
4
  class ManifestFiles < Check
5
5
  def call
6
6
  store.manifest.entries.each_with_object([]) do |entry, out|
7
- next if entry.nested
7
+ next if entry.nested?
8
8
 
9
9
  path = Textus::Key::Path.resolve(store.manifest, entry)
10
10
  next if File.exist?(path)
@@ -19,7 +19,7 @@ module Textus
19
19
  "code" => "protocol_mismatch",
20
20
  "severity" => "error",
21
21
  "message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
22
- "hint" => "Install textus 0.11.x to run the migrator, then upgrade to this version. See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110",
22
+ "hint" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
23
23
  }]
24
24
  end
25
25
 
@@ -38,7 +38,7 @@ module Textus
38
38
  "level" => "error",
39
39
  "subject" => path,
40
40
  "message" => "Store reports version=#{version.inspect}; this gem expects textus/3.",
41
- "fix" => "Install textus 0.11.x to run the migrator, then upgrade to this version. See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110",
41
+ "fix" => "Upgrade the store's manifest version to textus/3 (see CHANGELOG for breaking changes).",
42
42
  }]
43
43
  end
44
44
  end