textus 0.15.0 → 0.20.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +50 -55
  3. data/CHANGELOG.md +486 -0
  4. data/README.md +13 -9
  5. data/SPEC.md +13 -10
  6. data/docs/conventions.md +2 -2
  7. data/lib/textus/application/context.rb +20 -34
  8. data/lib/textus/application/policy/predicates/human_accept.rb +30 -0
  9. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  10. data/lib/textus/{domain → application}/policy/promotion.rb +20 -3
  11. data/lib/textus/application/projection.rb +91 -0
  12. data/lib/textus/application/reads/audit.rb +4 -4
  13. data/lib/textus/application/reads/blame.rb +11 -8
  14. data/lib/textus/application/reads/deps.rb +14 -3
  15. data/lib/textus/application/reads/freshness.rb +17 -6
  16. data/lib/textus/application/reads/get.rb +37 -11
  17. data/lib/textus/application/reads/get_or_refresh.rb +8 -8
  18. data/lib/textus/application/reads/list.rb +5 -3
  19. data/lib/textus/application/reads/policy_explain.rb +3 -3
  20. data/lib/textus/application/reads/published.rb +5 -3
  21. data/lib/textus/application/reads/rdeps.rb +15 -3
  22. data/lib/textus/application/reads/schema_envelope.rb +6 -3
  23. data/lib/textus/application/reads/stale.rb +3 -3
  24. data/lib/textus/application/reads/uid.rb +11 -3
  25. data/lib/textus/application/reads/validate_all.rb +12 -3
  26. data/lib/textus/application/reads/validator.rb +84 -0
  27. data/lib/textus/application/reads/where.rb +6 -3
  28. data/lib/textus/application/refresh/all.rb +16 -5
  29. data/lib/textus/application/refresh/orchestrator.rb +9 -9
  30. data/lib/textus/application/refresh/worker.rb +59 -32
  31. data/lib/textus/application/tools/migrate_keys.rb +191 -0
  32. data/lib/textus/application/tools/migrate_manifest_to_kinds.rb +31 -0
  33. data/lib/textus/application/writes/accept.rb +36 -13
  34. data/lib/textus/application/writes/delete.rb +13 -15
  35. data/lib/textus/application/writes/envelope_io.rb +166 -0
  36. data/lib/textus/application/writes/materializer.rb +50 -0
  37. data/lib/textus/application/writes/mv.rb +56 -95
  38. data/lib/textus/application/writes/publish.rb +132 -27
  39. data/lib/textus/application/writes/put.rb +17 -20
  40. data/lib/textus/application/writes/reject.rb +18 -9
  41. data/lib/textus/builder/pipeline.rb +21 -15
  42. data/lib/textus/builder/renderer/json.rb +4 -1
  43. data/lib/textus/builder/renderer/markdown.rb +7 -1
  44. data/lib/textus/builder/renderer/yaml.rb +4 -1
  45. data/lib/textus/cli/group/hook.rb +1 -3
  46. data/lib/textus/cli/group/key.rb +1 -4
  47. data/lib/textus/cli/group/refresh.rb +1 -2
  48. data/lib/textus/cli/group/rule.rb +1 -3
  49. data/lib/textus/cli/group/schema.rb +1 -5
  50. data/lib/textus/cli/group.rb +12 -16
  51. data/lib/textus/cli/verb/accept.rb +3 -1
  52. data/lib/textus/cli/verb/audit.rb +3 -1
  53. data/lib/textus/cli/verb/blame.rb +3 -1
  54. data/lib/textus/cli/verb/build.rb +4 -5
  55. data/lib/textus/cli/verb/delete.rb +3 -1
  56. data/lib/textus/cli/verb/deps.rb +3 -1
  57. data/lib/textus/cli/verb/doctor.rb +2 -0
  58. data/lib/textus/cli/verb/freshness.rb +3 -1
  59. data/lib/textus/cli/verb/get.rb +4 -2
  60. data/lib/textus/cli/verb/hook_run.rb +6 -4
  61. data/lib/textus/cli/verb/hooks.rb +8 -5
  62. data/lib/textus/cli/verb/init.rb +2 -0
  63. data/lib/textus/cli/verb/intro.rb +2 -0
  64. data/lib/textus/cli/verb/key_normalize.rb +35 -3
  65. data/lib/textus/cli/verb/list.rb +3 -1
  66. data/lib/textus/cli/verb/mv.rb +4 -1
  67. data/lib/textus/cli/verb/published.rb +3 -1
  68. data/lib/textus/cli/verb/put.rb +5 -4
  69. data/lib/textus/cli/verb/rdeps.rb +3 -1
  70. data/lib/textus/cli/verb/refresh.rb +1 -1
  71. data/lib/textus/cli/verb/refresh_stale.rb +4 -2
  72. data/lib/textus/cli/verb/reject.rb +3 -1
  73. data/lib/textus/cli/verb/rule_explain.rb +4 -1
  74. data/lib/textus/cli/verb/rule_list.rb +3 -0
  75. data/lib/textus/cli/verb/schema.rb +4 -1
  76. data/lib/textus/cli/verb/schema_diff.rb +3 -0
  77. data/lib/textus/cli/verb/schema_init.rb +3 -0
  78. data/lib/textus/cli/verb/schema_migrate.rb +3 -0
  79. data/lib/textus/cli/verb/uid.rb +4 -1
  80. data/lib/textus/cli/verb/where.rb +3 -1
  81. data/lib/textus/cli/verb.rb +30 -0
  82. data/lib/textus/cli.rb +18 -27
  83. data/lib/textus/doctor/check/audit_log.rb +1 -1
  84. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  85. data/lib/textus/doctor/check/hooks.rb +4 -2
  86. data/lib/textus/doctor/check/illegal_keys.rb +6 -5
  87. data/lib/textus/doctor/check/intake_registration.rb +5 -5
  88. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  89. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  90. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  91. data/lib/textus/doctor/check/sentinels.rb +2 -2
  92. data/lib/textus/doctor/check/templates.rb +4 -3
  93. data/lib/textus/doctor.rb +3 -4
  94. data/lib/textus/domain/authorizer.rb +37 -0
  95. data/lib/textus/domain/freshness/evaluator.rb +1 -1
  96. data/lib/textus/domain/freshness/policy.rb +1 -1
  97. data/lib/textus/domain/freshness/verdict.rb +1 -1
  98. data/lib/textus/domain/freshness.rb +40 -0
  99. data/lib/textus/{store → domain}/sentinel.rb +1 -1
  100. data/lib/textus/{store → domain}/staleness/generator_check.rb +9 -8
  101. data/lib/textus/{store → domain}/staleness/intake_check.rb +3 -3
  102. data/lib/textus/{store → domain}/staleness.rb +1 -1
  103. data/lib/textus/entry/json.rb +1 -1
  104. data/lib/textus/entry/markdown.rb +1 -1
  105. data/lib/textus/entry/yaml.rb +1 -1
  106. data/lib/textus/envelope.rb +7 -3
  107. data/lib/textus/errors.rb +19 -0
  108. data/lib/textus/hooks/builtin.rb +6 -6
  109. data/lib/textus/hooks/bus.rb +155 -0
  110. data/lib/textus/hooks/context.rb +38 -0
  111. data/lib/textus/hooks/fire_report.rb +23 -0
  112. data/lib/textus/hooks/loader.rb +20 -17
  113. data/lib/textus/{store → infra}/audit_log.rb +1 -1
  114. data/lib/textus/infra/audit_subscriber.rb +43 -0
  115. data/lib/textus/infra/event_bus.rb +3 -3
  116. data/lib/textus/infra/publisher.rb +3 -3
  117. data/lib/textus/infra/refresh/detached.rb +1 -1
  118. data/lib/textus/infra/storage/file_store.rb +26 -0
  119. data/lib/textus/init.rb +14 -11
  120. data/lib/textus/intro.rb +7 -7
  121. data/lib/textus/manifest/entry/base.rb +38 -0
  122. data/lib/textus/manifest/entry/derived.rb +25 -0
  123. data/lib/textus/manifest/entry/intake.rb +19 -0
  124. data/lib/textus/manifest/entry/leaf.rb +16 -0
  125. data/lib/textus/manifest/entry/nested.rb +39 -0
  126. data/lib/textus/manifest/entry/parser.rb +64 -31
  127. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  128. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  129. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  130. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  131. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  132. data/lib/textus/manifest/entry.rb +0 -72
  133. data/lib/textus/manifest/resolution.rb +5 -0
  134. data/lib/textus/manifest/resolver.rb +109 -0
  135. data/lib/textus/manifest/schema.rb +1 -1
  136. data/lib/textus/manifest.rb +4 -100
  137. data/lib/textus/operations.rb +147 -23
  138. data/lib/textus/schema/tools.rb +7 -7
  139. data/lib/textus/schemas.rb +46 -0
  140. data/lib/textus/store.rb +12 -49
  141. data/lib/textus/uid.rb +18 -0
  142. data/lib/textus/version.rb +1 -1
  143. data/lib/textus.rb +17 -1
  144. metadata +31 -23
  145. data/lib/textus/application/writes/build.rb +0 -79
  146. data/lib/textus/dependencies.rb +0 -23
  147. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  148. data/lib/textus/hooks/dispatcher.rb +0 -63
  149. data/lib/textus/hooks/dsl.rb +0 -11
  150. data/lib/textus/hooks/registry.rb +0 -81
  151. data/lib/textus/migrate_keys.rb +0 -187
  152. data/lib/textus/operations/reads.rb +0 -56
  153. data/lib/textus/operations/refresh.rb +0 -27
  154. data/lib/textus/operations/writes.rb +0 -21
  155. data/lib/textus/projection.rb +0 -89
  156. data/lib/textus/refresh.rb +0 -39
  157. data/lib/textus/store/reader.rb +0 -69
  158. data/lib/textus/store/validator.rb +0 -82
  159. data/lib/textus/store/writer.rb +0 -102
data/lib/textus/store.rb CHANGED
@@ -1,16 +1,8 @@
1
1
  require "fileutils"
2
- require "securerandom"
3
2
 
4
3
  module Textus
5
4
  class Store
6
- attr_reader :root, :manifest, :registry, :reader, :writer, :bus
7
-
8
- # A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
9
- # short on purpose. Random enough for collision-never-in-practice within a
10
- # single store.
11
- def self.mint_uid
12
- SecureRandom.hex(8)
13
- end
5
+ attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus
14
6
 
15
7
  def self.discover(start_dir = Dir.pwd, root: nil)
16
8
  explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
@@ -37,46 +29,17 @@ module Textus
37
29
  end
38
30
 
39
31
  def initialize(root)
40
- @root = File.expand_path(root)
41
- @manifest = Manifest.load(@root)
42
- @bus = Hooks::Dispatcher.new(audit_log: audit_log)
43
- @registry = Hooks::Registry.new(dispatcher: @bus)
44
- @schemas = {}
45
- load_hooks
46
- @reader = Reader.new(self)
47
- @writer = Writer.new(self)
48
- @bus.publish(:store_loaded, store: Textus::Application::Context.system(self))
49
- end
50
-
51
- def load_hooks
52
- Textus.with_registry(@registry) do
53
- Hooks::Builtin.register_all
54
- dir = File.join(@root, "hooks")
55
- return unless File.directory?(dir)
56
-
57
- Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
58
- begin
59
- load(f)
60
- rescue StandardError, ScriptError => e
61
- raise UsageError.new("failed loading hook #{File.basename(f)}: #{e.class}: #{e.message}")
62
- end
63
- end
64
- end
65
- end
66
-
67
- def schema_for(name)
68
- return nil if name.nil?
69
-
70
- @schemas[name] ||= begin
71
- sp = File.join(@root, "schemas", "#{name}.yaml")
72
- raise IoError.new("schema not found: #{sp}") unless File.exist?(sp)
73
-
74
- Schema.load(sp)
75
- end
76
- end
77
-
78
- def audit_log
79
- @audit_log ||= Store::AuditLog.new(@root)
32
+ @root = File.expand_path(root)
33
+ @manifest = Manifest.load(@root)
34
+ @schemas = Schemas.new(File.join(@root, "schemas"))
35
+ @file_store = Infra::Storage::FileStore.new
36
+ @audit_log = Infra::AuditLog.new(@root)
37
+ @bus = Hooks::Bus.new
38
+ Infra::AuditSubscriber.new(@audit_log).attach(@bus)
39
+ Hooks::Builtin.register_all(@bus)
40
+ Hooks::Loader.new(bus: @bus).load_dir(File.join(@root, "hooks"))
41
+ ops = Operations.for(self, role: Role::DEFAULT)
42
+ @bus.publish(:store_loaded, ctx: ops.hook_context)
80
43
  end
81
44
  end
82
45
  end
data/lib/textus/uid.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "securerandom"
2
+
3
+ module Textus
4
+ # A Textus UID: 16 lowercase hex chars (SecureRandom.hex(8)). Not a UUID —
5
+ # short on purpose. Random enough for collision-never-in-practice within a
6
+ # single store.
7
+ module Uid
8
+ module_function
9
+
10
+ def mint
11
+ SecureRandom.hex(8)
12
+ end
13
+
14
+ def valid?(str)
15
+ str.is_a?(String) && str.match?(/\A[0-9a-f]{16}\z/)
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.15.0"
2
+ VERSION = "0.20.0"
3
3
  PROTOCOL = "textus/3"
4
4
  end
data/lib/textus.rb CHANGED
@@ -8,11 +8,27 @@ loader.inflector.inflect(
8
8
  "json" => "Json",
9
9
  "yaml" => "Yaml",
10
10
  "hook_dsl_scanner" => "HookDSLScanner",
11
+ "envelope_io" => "EnvelopeIO",
11
12
  )
12
13
  loader.ignore(File.expand_path("textus/errors.rb", __dir__))
13
14
  loader.setup
14
15
  loader.eager_load
15
16
 
16
17
  module Textus
17
- extend Hooks::Dsl
18
+ @hook_mutex = Mutex.new
19
+ @hook_blocks = []
20
+
21
+ def self.hook(&blk)
22
+ raise UsageError.new("hook block required") unless blk
23
+
24
+ @hook_mutex.synchronize { @hook_blocks << blk }
25
+ end
26
+
27
+ def self.drain_hook_blocks
28
+ @hook_mutex.synchronize do
29
+ blocks = @hook_blocks
30
+ @hook_blocks = []
31
+ blocks
32
+ end
33
+ end
18
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -110,6 +110,10 @@ files:
110
110
  - exe/textus
111
111
  - lib/textus.rb
112
112
  - lib/textus/application/context.rb
113
+ - lib/textus/application/policy/predicates/human_accept.rb
114
+ - lib/textus/application/policy/predicates/schema_valid.rb
115
+ - lib/textus/application/policy/promotion.rb
116
+ - lib/textus/application/projection.rb
113
117
  - lib/textus/application/reads/audit.rb
114
118
  - lib/textus/application/reads/blame.rb
115
119
  - lib/textus/application/reads/deps.rb
@@ -124,13 +128,17 @@ files:
124
128
  - lib/textus/application/reads/stale.rb
125
129
  - lib/textus/application/reads/uid.rb
126
130
  - lib/textus/application/reads/validate_all.rb
131
+ - lib/textus/application/reads/validator.rb
127
132
  - lib/textus/application/reads/where.rb
128
133
  - lib/textus/application/refresh/all.rb
129
134
  - lib/textus/application/refresh/orchestrator.rb
130
135
  - lib/textus/application/refresh/worker.rb
136
+ - lib/textus/application/tools/migrate_keys.rb
137
+ - lib/textus/application/tools/migrate_manifest_to_kinds.rb
131
138
  - lib/textus/application/writes/accept.rb
132
- - lib/textus/application/writes/build.rb
133
139
  - lib/textus/application/writes/delete.rb
140
+ - lib/textus/application/writes/envelope_io.rb
141
+ - lib/textus/application/writes/materializer.rb
134
142
  - lib/textus/application/writes/mv.rb
135
143
  - lib/textus/application/writes/publish.rb
136
144
  - lib/textus/application/writes/put.rb
@@ -179,7 +187,6 @@ files:
179
187
  - lib/textus/cli/verb/schema_migrate.rb
180
188
  - lib/textus/cli/verb/uid.rb
181
189
  - lib/textus/cli/verb/where.rb
182
- - lib/textus/dependencies.rb
183
190
  - lib/textus/doctor.rb
184
191
  - lib/textus/doctor/check.rb
185
192
  - lib/textus/doctor/check/audit_log.rb
@@ -198,6 +205,8 @@ files:
198
205
  - lib/textus/doctor/check/templates.rb
199
206
  - lib/textus/doctor/check/unowned_schema_fields.rb
200
207
  - lib/textus/domain/action.rb
208
+ - lib/textus/domain/authorizer.rb
209
+ - lib/textus/domain/freshness.rb
201
210
  - lib/textus/domain/freshness/evaluator.rb
202
211
  - lib/textus/domain/freshness/policy.rb
203
212
  - lib/textus/domain/freshness/verdict.rb
@@ -206,11 +215,12 @@ files:
206
215
  - lib/textus/domain/policy.rb
207
216
  - lib/textus/domain/policy/handler_allowlist.rb
208
217
  - lib/textus/domain/policy/matcher.rb
209
- - lib/textus/domain/policy/predicates/human_accept.rb
210
- - lib/textus/domain/policy/predicates/schema_valid.rb
211
218
  - lib/textus/domain/policy/promote.rb
212
- - lib/textus/domain/policy/promotion.rb
213
219
  - lib/textus/domain/policy/refresh.rb
220
+ - lib/textus/domain/sentinel.rb
221
+ - lib/textus/domain/staleness.rb
222
+ - lib/textus/domain/staleness/generator_check.rb
223
+ - lib/textus/domain/staleness/intake_check.rb
214
224
  - lib/textus/entry.rb
215
225
  - lib/textus/entry/base.rb
216
226
  - lib/textus/entry/json.rb
@@ -221,16 +231,19 @@ files:
221
231
  - lib/textus/errors.rb
222
232
  - lib/textus/etag.rb
223
233
  - lib/textus/hooks/builtin.rb
224
- - lib/textus/hooks/dispatcher.rb
225
- - lib/textus/hooks/dsl.rb
234
+ - lib/textus/hooks/bus.rb
235
+ - lib/textus/hooks/context.rb
236
+ - lib/textus/hooks/fire_report.rb
226
237
  - lib/textus/hooks/loader.rb
227
- - lib/textus/hooks/registry.rb
238
+ - lib/textus/infra/audit_log.rb
239
+ - lib/textus/infra/audit_subscriber.rb
228
240
  - lib/textus/infra/build_lock.rb
229
241
  - lib/textus/infra/clock.rb
230
242
  - lib/textus/infra/event_bus.rb
231
243
  - lib/textus/infra/publisher.rb
232
244
  - lib/textus/infra/refresh/detached.rb
233
245
  - lib/textus/infra/refresh/lock.rb
246
+ - lib/textus/infra/storage/file_store.rb
234
247
  - lib/textus/init.rb
235
248
  - lib/textus/intro.rb
236
249
  - lib/textus/key/distance.rb
@@ -238,6 +251,11 @@ files:
238
251
  - lib/textus/key/path.rb
239
252
  - lib/textus/manifest.rb
240
253
  - lib/textus/manifest/entry.rb
254
+ - lib/textus/manifest/entry/base.rb
255
+ - lib/textus/manifest/entry/derived.rb
256
+ - lib/textus/manifest/entry/intake.rb
257
+ - lib/textus/manifest/entry/leaf.rb
258
+ - lib/textus/manifest/entry/nested.rb
241
259
  - lib/textus/manifest/entry/parser.rb
242
260
  - lib/textus/manifest/entry/validators.rb
243
261
  - lib/textus/manifest/entry/validators/events.rb
@@ -245,28 +263,18 @@ files:
245
263
  - lib/textus/manifest/entry/validators/index_filename.rb
246
264
  - lib/textus/manifest/entry/validators/inject_intro.rb
247
265
  - lib/textus/manifest/entry/validators/publish_each.rb
266
+ - lib/textus/manifest/resolution.rb
267
+ - lib/textus/manifest/resolver.rb
248
268
  - lib/textus/manifest/rules.rb
249
269
  - lib/textus/manifest/schema.rb
250
- - lib/textus/migrate_keys.rb
251
270
  - lib/textus/mustache.rb
252
271
  - lib/textus/operations.rb
253
- - lib/textus/operations/reads.rb
254
- - lib/textus/operations/refresh.rb
255
- - lib/textus/operations/writes.rb
256
- - lib/textus/projection.rb
257
- - lib/textus/refresh.rb
258
272
  - lib/textus/role.rb
259
273
  - lib/textus/schema.rb
260
274
  - lib/textus/schema/tools.rb
275
+ - lib/textus/schemas.rb
261
276
  - lib/textus/store.rb
262
- - lib/textus/store/audit_log.rb
263
- - lib/textus/store/reader.rb
264
- - lib/textus/store/sentinel.rb
265
- - lib/textus/store/staleness.rb
266
- - lib/textus/store/staleness/generator_check.rb
267
- - lib/textus/store/staleness/intake_check.rb
268
- - lib/textus/store/validator.rb
269
- - lib/textus/store/writer.rb
277
+ - lib/textus/uid.rb
270
278
  - lib/textus/version.rb
271
279
  homepage: https://github.com/patrick204nqh/textus
272
280
  licenses:
@@ -1,79 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- module Application
5
- module Writes
6
- # Materializes generator-zone entries (template + projection) onto disk
7
- # and copies the result to any configured `publish_to:` targets. Fires
8
- # `:build_completed` and `:file_published` events.
9
- #
10
- # For `publish_each:` (per-leaf publishing of nested entries), see
11
- # `Application::Writes::Publish`. The CLI verb `textus build` calls
12
- # both classes and merges the results.
13
- class Build
14
- def initialize(ctx:, bus:)
15
- @ctx = ctx
16
- @bus = bus
17
- end
18
-
19
- def call(prefix: nil)
20
- built = manifest.entries.filter_map do |mentry|
21
- next unless mentry.in_generator_zone?
22
- next unless mentry.projection || mentry.template
23
- next if prefix && !mentry.key.start_with?(prefix)
24
-
25
- materialize(mentry)
26
- end
27
- { "protocol" => Textus::PROTOCOL, "built" => built }
28
- end
29
-
30
- private
31
-
32
- def store = @ctx.store
33
- def manifest = store.manifest
34
- def root = store.root
35
-
36
- def materialize(mentry)
37
- target_path = Builder::Pipeline.run(
38
- store: store,
39
- mentry: mentry,
40
- template_loader: ->(name) { read_template(name) },
41
- )
42
- publish_and_fire(mentry, target_path)
43
- { "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
44
- end
45
-
46
- def read_template(name)
47
- tpl_path = File.join(root, "templates", name)
48
- raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
49
-
50
- File.read(tpl_path)
51
- end
52
-
53
- def publish_and_fire(mentry, target_path)
54
- envelope = store.reader.get(mentry.key)
55
- repo_root = File.dirname(root)
56
-
57
- mentry.publish_to.each do |rel|
58
- target_abs = File.join(repo_root, rel)
59
- Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: root)
60
- publish_event(:file_published,
61
- key: mentry.key,
62
- envelope: envelope,
63
- source: target_path,
64
- target: target_abs)
65
- end
66
-
67
- publish_event(:build_completed,
68
- key: mentry.key,
69
- envelope: envelope,
70
- sources: Array(mentry.projection&.fetch("select", nil)).compact)
71
- end
72
-
73
- def publish_event(event, **payload)
74
- @bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,23 +0,0 @@
1
- module Textus
2
- module Dependencies
3
- def self.deps_of(manifest, key)
4
- entry = manifest.entries.find { |e| e.key == key } or return []
5
- result = Array(entry.projection&.fetch("select", nil)).map { |s| s }
6
- Array(entry.generator&.fetch("sources", nil)).each { |s| result << s }
7
- result.uniq
8
- end
9
-
10
- def self.rdeps_of(manifest, key)
11
- manifest.entries.each_with_object([]) do |e, acc|
12
- sources = Array(e.projection&.fetch("select", nil)) + Array(e.generator&.fetch("sources", nil))
13
- acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
14
- end
15
- end
16
-
17
- def self.published_of(manifest)
18
- manifest.entries.reject { |e| e.publish_to.empty? }.map do |e|
19
- { "key" => e.key, "publish_to" => e.publish_to }
20
- end
21
- end
22
- end
23
- end
@@ -1,31 +0,0 @@
1
- module Textus
2
- module Domain
3
- module Policy
4
- module Predicates
5
- class HumanAccept
6
- attr_reader :reason
7
-
8
- def name
9
- "human_accept"
10
- end
11
-
12
- # The role is passed via `store` (an Application::Context-like object
13
- # with a `role` reader) or through the entry metadata. In practice,
14
- # Accept already enforces role == "human" before reaching the
15
- # promotion gate, so this predicate trivially passes. It documents
16
- # intent and future-proofs multi-actor accept flows.
17
- def call(store:, entry: nil) # rubocop:disable Lint/UnusedMethodArgument
18
- role = store.respond_to?(:role) ? store.role.to_s : nil
19
- # If we cannot determine the role (e.g. store doesn't expose it),
20
- # we trust that Accept has already checked — allow through.
21
- return true if role.nil?
22
-
23
- ok = (role == "human")
24
- @reason = "current role is '#{role}', expected 'human'" unless ok
25
- ok
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "timeout"
4
-
5
- module Textus
6
- module Hooks
7
- class Dispatcher
8
- HOOK_TIMEOUT_SECONDS = 2
9
-
10
- def initialize(audit_log:)
11
- @audit_log = audit_log
12
- @subscribers = Hash.new { |h, k| h[k] = [] }
13
- end
14
-
15
- def subscribe(event, name, keys: nil, &block)
16
- @subscribers[event.to_sym] << { name: name.to_sym, callable: block, keys: keys }
17
- end
18
-
19
- def publish(event, **kwargs)
20
- key = kwargs[:key] || "-"
21
- @subscribers[event.to_sym].each do |sub|
22
- next unless match?(sub[:keys], key)
23
-
24
- invoke(event, sub, key, kwargs)
25
- end
26
- end
27
-
28
- private
29
-
30
- def invoke(event, sub, key, kwargs)
31
- accepted = filter_kwargs(sub[:callable], kwargs)
32
- Timeout.timeout(HOOK_TIMEOUT_SECONDS) { sub[:callable].call(**accepted) }
33
- rescue StandardError => e
34
- extras = { "event" => event.to_s, "hook" => sub[:name].to_s, "error" => "#{e.class}: #{e.message}" }
35
- extras["target_key"] = kwargs[:target_key] if kwargs.key?(:target_key)
36
- extras["pending_key"] = kwargs[:pending_key] if kwargs.key?(:pending_key)
37
- @audit_log.append(
38
- role: "runner", verb: "event_error", key: key,
39
- etag_before: nil, etag_after: nil, extras: extras
40
- )
41
- end
42
-
43
- # Passes only the kwargs a hook block declares. Lets us extend event
44
- # payloads (e.g., correlation_id) without breaking hooks written against
45
- # the old signature.
46
- def filter_kwargs(callable, kwargs)
47
- params = callable.parameters
48
- return kwargs if params.any? { |type, _| type == :keyrest }
49
-
50
- accepted = params.each_with_object([]) do |(type, name), acc|
51
- acc << name if %i[key keyreq].include?(type)
52
- end
53
- kwargs.slice(*accepted)
54
- end
55
-
56
- def match?(globs, key)
57
- return true if globs.nil?
58
-
59
- Array(globs).any? { |g| File.fnmatch?(g, key.to_s, File::FNM_PATHNAME) }
60
- end
61
- end
62
- end
63
- end
@@ -1,11 +0,0 @@
1
- module Textus
2
- module Hooks
3
- module Dsl
4
- def on(event, name, **, &blk)
5
- raise UsageError.new("hook needs a block") unless blk
6
-
7
- Loader.current_registry.register(event, name, **, &blk)
8
- end
9
- end
10
- end
11
- end
@@ -1,81 +0,0 @@
1
- module Textus
2
- module Hooks
3
- class Registry
4
- EVENTS = {
5
- # RPC: exactly 1 handler per name; return value flows into store; failure aborts.
6
- resolve_intake: { mode: :rpc, args: %i[store config args] },
7
- transform_rows: { mode: :rpc, args: %i[store rows config] },
8
- validate: { mode: :rpc, args: %i[store] },
9
-
10
- # Pub-sub: 0..N handlers per event; return discarded; failure logged to audit.
11
- entry_put: { mode: :pubsub, args: %i[store key envelope] },
12
- entry_deleted: { mode: :pubsub, args: %i[store key] },
13
- entry_refreshed: { mode: :pubsub, args: %i[store key envelope change] },
14
- entry_renamed: { mode: :pubsub, args: %i[store key from_key to_key envelope] },
15
- build_completed: { mode: :pubsub, args: %i[store key envelope sources] },
16
- proposal_accepted: { mode: :pubsub, args: %i[store key target_key] },
17
- proposal_rejected: { mode: :pubsub, args: %i[store key target_key] },
18
- file_published: { mode: :pubsub, args: %i[store key envelope source target] },
19
- store_loaded: { mode: :pubsub, args: %i[store] },
20
- refresh_started: { mode: :pubsub, args: %i[store key mode] },
21
- refresh_failed: { mode: :pubsub, args: %i[store key error_class error_message] },
22
- refresh_backgrounded: { mode: :pubsub, args: %i[store key started_at budget_ms] },
23
- }.freeze
24
-
25
- def initialize(dispatcher: nil)
26
- @rpc = Hash.new { |h, k| h[k] = {} } # event => { name => callable }
27
- @pubsub = Hash.new { |h, k| h[k] = [] } # event => [{name:, callable:, keys:}]
28
- @dispatcher = dispatcher
29
- end
30
-
31
- def register(event, name, keys: nil, &blk)
32
- event_sym = event.to_sym
33
- spec = EVENTS[event_sym] or raise UsageError.new("unknown event: #{event}")
34
- shape_check!(event_sym, spec, blk)
35
- name = name.to_sym
36
-
37
- case spec[:mode]
38
- when :rpc
39
- raise UsageError.new("#{event_sym} '#{name}' already registered") if @rpc[event_sym].key?(name)
40
-
41
- @rpc[event_sym][name] = blk
42
- when :pubsub
43
- raise UsageError.new("#{event_sym} hook '#{name}' already registered") if @pubsub[event_sym].any? { |h| h[:name] == name }
44
-
45
- @pubsub[event_sym] << { name: name, callable: blk, keys: keys }
46
- @dispatcher&.subscribe(event_sym, name, keys: keys, &blk)
47
- end
48
- end
49
-
50
- def rpc_callable(event, name)
51
- @rpc[event.to_sym][name.to_sym] or
52
- raise UsageError.new("unknown #{event}: #{name}")
53
- end
54
-
55
- def listeners(event, key:)
56
- @pubsub[event.to_sym].select { |h| h[:keys].nil? || matches_any?(h[:keys], key) }
57
- end
58
-
59
- def rpc_names(event) = @rpc[event.to_sym].keys
60
- def pubsub_handlers(event) = @pubsub[event.to_sym]
61
-
62
- private
63
-
64
- def shape_check!(event, spec, blk)
65
- required = spec[:args]
66
- provided = blk.parameters.select { |t, _| %i[keyreq key keyrest].include?(t) } # rubocop:disable Style/HashSlice
67
- keyrest = provided.any? { |t, _| t == :keyrest }
68
- missing = required - provided.map { |_, n| n }
69
- return if keyrest || missing.empty?
70
-
71
- raise UsageError.new(
72
- "#{event} hooks must accept kwargs: #{required.join(", ")} (missing: #{missing.join(", ")})",
73
- )
74
- end
75
-
76
- def matches_any?(globs, key)
77
- Array(globs).any? { |g| File.fnmatch?(g, key.to_s, File::FNM_PATHNAME) }
78
- end
79
- end
80
- end
81
- end