textus 0.18.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +43 -48
  3. data/CHANGELOG.md +173 -0
  4. data/lib/textus/application/context.rb +20 -58
  5. data/lib/textus/application/policy/predicates/human_accept.rb +30 -0
  6. data/lib/textus/{domain → application}/policy/predicates/schema_valid.rb +5 -5
  7. data/lib/textus/{domain → application}/policy/promotion.rb +20 -3
  8. data/lib/textus/application/projection.rb +91 -0
  9. data/lib/textus/application/reads/audit.rb +4 -4
  10. data/lib/textus/application/reads/blame.rb +9 -8
  11. data/lib/textus/application/reads/deps.rb +14 -3
  12. data/lib/textus/application/reads/freshness.rb +10 -8
  13. data/lib/textus/application/reads/get.rb +10 -8
  14. data/lib/textus/application/reads/get_or_refresh.rb +3 -3
  15. data/lib/textus/application/reads/list.rb +3 -3
  16. data/lib/textus/application/reads/policy_explain.rb +3 -3
  17. data/lib/textus/application/reads/published.rb +5 -3
  18. data/lib/textus/application/reads/rdeps.rb +15 -3
  19. data/lib/textus/application/reads/schema_envelope.rb +5 -4
  20. data/lib/textus/application/reads/stale.rb +3 -3
  21. data/lib/textus/application/reads/uid.rb +11 -3
  22. data/lib/textus/application/reads/validate_all.rb +10 -6
  23. data/lib/textus/application/reads/validator.rb +2 -2
  24. data/lib/textus/application/reads/where.rb +3 -3
  25. data/lib/textus/application/refresh/all.rb +15 -11
  26. data/lib/textus/application/refresh/orchestrator.rb +9 -8
  27. data/lib/textus/application/refresh/worker.rb +56 -32
  28. data/lib/textus/application/tools/migrate_keys.rb +191 -0
  29. data/lib/textus/application/tools/migrate_manifest_to_kinds.rb +31 -0
  30. data/lib/textus/application/writes/accept.rb +38 -15
  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 +20 -11
  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 +2 -5
  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/key_normalize.rb +32 -3
  47. data/lib/textus/cli/verb/put.rb +2 -3
  48. data/lib/textus/cli/verb/refresh_stale.rb +1 -2
  49. data/lib/textus/doctor/check/handler_allowlist.rb +3 -2
  50. data/lib/textus/doctor/check/hooks.rb +2 -2
  51. data/lib/textus/doctor/check/illegal_keys.rb +6 -5
  52. data/lib/textus/doctor/check/intake_registration.rb +2 -2
  53. data/lib/textus/doctor/check/manifest_files.rb +1 -1
  54. data/lib/textus/doctor/check/protocol_version.rb +2 -2
  55. data/lib/textus/doctor/check/templates.rb +4 -3
  56. data/lib/textus/doctor.rb +3 -4
  57. data/lib/textus/domain/authorizer.rb +37 -0
  58. data/lib/textus/domain/staleness/generator_check.rb +8 -7
  59. data/lib/textus/domain/staleness/intake_check.rb +2 -2
  60. data/lib/textus/hooks/builtin.rb +6 -6
  61. data/lib/textus/hooks/bus.rb +155 -0
  62. data/lib/textus/hooks/context.rb +38 -0
  63. data/lib/textus/hooks/fire_report.rb +23 -0
  64. data/lib/textus/hooks/loader.rb +3 -3
  65. data/lib/textus/infra/audit_subscriber.rb +4 -4
  66. data/lib/textus/infra/event_bus.rb +3 -3
  67. data/lib/textus/infra/refresh/detached.rb +1 -1
  68. data/lib/textus/init.rb +3 -2
  69. data/lib/textus/intro.rb +7 -7
  70. data/lib/textus/manifest/entry/base.rb +38 -0
  71. data/lib/textus/manifest/entry/derived.rb +25 -0
  72. data/lib/textus/manifest/entry/intake.rb +19 -0
  73. data/lib/textus/manifest/entry/leaf.rb +16 -0
  74. data/lib/textus/manifest/entry/nested.rb +39 -0
  75. data/lib/textus/manifest/entry/parser.rb +64 -31
  76. data/lib/textus/manifest/entry/validators/events.rb +3 -2
  77. data/lib/textus/manifest/entry/validators/format_matrix.rb +5 -3
  78. data/lib/textus/manifest/entry/validators/index_filename.rb +11 -10
  79. data/lib/textus/manifest/entry/validators/inject_intro.rb +5 -2
  80. data/lib/textus/manifest/entry/validators/publish_each.rb +9 -6
  81. data/lib/textus/manifest/entry.rb +0 -72
  82. data/lib/textus/manifest/resolver.rb +109 -0
  83. data/lib/textus/manifest/schema.rb +1 -1
  84. data/lib/textus/manifest.rb +3 -100
  85. data/lib/textus/operations.rb +131 -74
  86. data/lib/textus/schema/tools.rb +2 -2
  87. data/lib/textus/store.rb +6 -6
  88. data/lib/textus/version.rb +1 -1
  89. metadata +18 -11
  90. data/lib/textus/application/writes/build.rb +0 -78
  91. data/lib/textus/dependencies.rb +0 -23
  92. data/lib/textus/domain/policy/predicates/human_accept.rb +0 -31
  93. data/lib/textus/hooks/dispatcher.rb +0 -71
  94. data/lib/textus/hooks/registry.rb +0 -85
  95. data/lib/textus/migrate_keys.rb +0 -187
  96. data/lib/textus/projection.rb +0 -89
  97. data/lib/textus/refresh.rb +0 -39
@@ -4,7 +4,7 @@ module Textus
4
4
  ROOT_KEYS = %w[version zones entries rules].freeze
5
5
  ZONE_KEYS = %w[name write_policy read_policy].freeze
6
6
  ENTRY_KEYS = %w[
7
- key path zone schema owner nested format
7
+ key path zone kind schema owner nested format
8
8
  compute template publish_to publish_each
9
9
  intake events inject_intro index_filename
10
10
  ].freeze
@@ -1,18 +1,10 @@
1
1
  require "yaml"
2
2
  require_relative "manifest/schema"
3
3
  require_relative "manifest/resolution"
4
+ require_relative "manifest/resolver"
4
5
 
5
6
  module Textus
6
7
  class Manifest
7
- TEXTUS_2_HINT = "Install textus 0.11.x to run the migrator, then upgrade to this version. " \
8
- "See https://github.com/patrick204nqh/textus/blob/main/CHANGELOG.md#0110".freeze
9
-
10
- def self.version_hint_for(version)
11
- version == "textus/2" ? TEXTUS_2_HINT : nil
12
- end
13
-
14
- private_class_method :version_hint_for
15
-
16
8
  attr_reader :root, :entries, :raw
17
9
 
18
10
  def zones
@@ -59,7 +51,6 @@ module Textus
59
51
  raise BadFrontmatter.new(
60
52
  source,
61
53
  "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
62
- hint: version_hint_for(raw["version"]),
63
54
  )
64
55
  end
65
56
  private_class_method :check_version!
@@ -87,53 +78,8 @@ module Textus
87
78
  rules.for(key)
88
79
  end
89
80
 
90
- # Returns a Resolution(entry:, path:, remaining:) value object.
91
- def resolve(key)
92
- validate_key!(key)
93
- segments = key.split(".")
94
- # longest-prefix match
95
- candidates = @entries
96
- .map { |e| [e, e.key.split(".")] }
97
- .select { |(_, esegs)| esegs == segments[0, esegs.length] }
98
- .sort_by { |(_, esegs)| -esegs.length }
99
- raise UnknownKey.new(key, suggestions: suggestions_for(key)) if candidates.empty?
100
-
101
- entry, esegs = candidates.first
102
- remaining = segments[esegs.length..]
103
- if remaining.empty?
104
- path = resolve_leaf_path(entry)
105
- Resolution.new(entry: entry, path: path, remaining: [])
106
- else
107
- raise UnknownKey.new(key, suggestions: suggestions_for(key)) unless entry.nested
108
-
109
- path = if entry.index_filename
110
- File.join(@root, "zones", entry.path, *remaining, entry.index_filename)
111
- else
112
- primary_ext = Textus::Entry.for_format(entry.format).extensions.first
113
- File.join(@root, "zones", entry.path, *remaining) + primary_ext
114
- end
115
- Resolution.new(entry: entry, path: path, remaining: remaining)
116
- end
117
- end
118
-
119
- # Returns up to 5 dotted keys from the manifest that look similar to the
120
- # requested key, ranked by shared-prefix length then Levenshtein distance.
121
- def suggestions_for(key)
122
- candidates = enumerate.map { |r| r[:key] }
123
- # Include declared (non-nested) entry keys even if file is missing.
124
- candidates.concat(@entries.reject(&:nested).map(&:key))
125
- candidates.uniq!
126
- Key::Distance.suggest(key, candidates, limit: 5)
127
- rescue StandardError
128
- []
129
- end
130
-
131
- # Enumerate all entry files reachable through the manifest. Returns
132
- # [{ key:, path:, manifest_entry: }, ...]
133
- def enumerate(prefix: nil)
134
- out = @entries.flat_map { |entry| entry.nested ? enumerate_nested(entry) : enumerate_leaf(entry) }
135
- out.select! { |row| row[:key] == prefix || row[:key].start_with?("#{prefix}.") } if prefix
136
- out.sort_by { |row| row[:key] }
81
+ def resolver
82
+ @resolver ||= Resolver.new(self)
137
83
  end
138
84
 
139
85
  def validate_key!(key)
@@ -144,51 +90,8 @@ module Textus
144
90
 
145
91
  private
146
92
 
147
- def enumerate_leaf(entry)
148
- fp = resolve_leaf_path(entry)
149
- File.exist?(fp) ? [{ key: entry.key, path: fp, manifest_entry: entry }] : []
150
- end
151
-
152
- def enumerate_nested(entry)
153
- base = File.join(@root, "zones", entry.path)
154
- return [] unless File.directory?(base)
155
-
156
- glob_pattern = entry.index_filename ? "**/#{entry.index_filename}" : nested_glob(entry.format)
157
- Dir.glob(File.join(base, glob_pattern)).filter_map { |path| nested_row_for(entry, base, path) }
158
- end
159
-
160
- def nested_row_for(entry, base, path)
161
- rel = path.sub(%r{\A#{Regexp.escape(base)}/?}, "")
162
- stripped = entry.index_filename ? File.dirname(rel) : rel.sub(/#{Regexp.escape(File.extname(rel))}\z/, "")
163
- segs = stripped.split("/").reject { |s| s.empty? || s == "." }
164
- return nil if segs.empty?
165
-
166
- illegal = segs.find { |s| !valid_segment?(s) }
167
- if illegal
168
- warn("textus: skipping illegal key segment '#{illegal}' at #{path} — run 'textus key normalize --dry-run'")
169
- return nil
170
- end
171
-
172
- { key: (entry.key.split(".") + segs).join("."), path: path, manifest_entry: entry }
173
- end
174
-
175
- def valid_segment?(seg)
176
- return false if seg.nil? || seg.empty?
177
- return false if seg.length > Key::Grammar::MAX_SEGMENT_LEN
178
-
179
- seg.match?(Key::Grammar::SEGMENT)
180
- end
181
-
182
93
  def validate_declared_keys!
183
94
  @entries.each { |e| validate_key!(e.key) }
184
95
  end
185
-
186
- def resolve_leaf_path(entry)
187
- Textus::Key::Path.resolve(self, entry)
188
- end
189
-
190
- def nested_glob(format)
191
- Textus::Entry.for_format(format).nested_glob
192
- end
193
96
  end
194
97
  end
@@ -10,102 +10,159 @@ module Textus
10
10
  # ops.refresh_all(prefix: ..., zone: ...)
11
11
  class Operations
12
12
  def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
13
- ctx = Application::Context.new(
13
+ new(
14
+ ctx: Application::Context.build(role: role, correlation_id: correlation_id, dry_run: dry_run),
15
+ manifest: store.manifest,
16
+ file_store: store.file_store,
17
+ schemas: store.schemas,
18
+ audit_log: store.audit_log,
19
+ bus: store.bus,
20
+ root: store.root,
14
21
  store: store,
15
- role: role,
16
- correlation_id: correlation_id,
17
- dry_run: dry_run,
18
22
  )
19
- new(ctx)
20
23
  end
21
24
 
22
- attr_reader :ctx
25
+ attr_reader :ctx, :store
23
26
 
24
- def initialize(ctx)
25
- @ctx = ctx
27
+ # rubocop:disable Metrics/ParameterLists
28
+ def initialize(ctx:, manifest:, file_store:, schemas:, audit_log:, bus:, root:, store:)
29
+ @ctx = ctx
30
+ @manifest = manifest
31
+ @file_store = file_store
32
+ @schemas = schemas
33
+ @audit_log = audit_log
34
+ @bus = bus
35
+ @root = root
36
+ @store = store
37
+ @authorizer = Textus::Domain::Authorizer.new(manifest: @manifest)
26
38
  end
39
+ # rubocop:enable Metrics/ParameterLists
27
40
 
28
- def with_role(role) = self.class.new(@ctx.with_role(role))
41
+ def with_role(role)
42
+ self.class.new(
43
+ ctx: @ctx.with_role(role),
44
+ manifest: @manifest, file_store: @file_store, schemas: @schemas,
45
+ audit_log: @audit_log, bus: @bus,
46
+ root: @root, store: @store
47
+ )
48
+ end
49
+
50
+ def hook_context
51
+ @hook_context ||= Textus::Hooks::Context.new(ops: self)
52
+ end
29
53
 
30
54
  # writes
31
- def put(...) = put_op.call(...)
32
- def delete(...) = delete_op.call(...)
33
- def mv(...) = mv_op.call(...)
34
- def accept(...) = accept_op.call(...)
35
- def reject(...) = reject_op.call(...)
36
- def build(...) = build_op.call(...)
37
- def publish(...) = publish_op.call(...)
55
+ def put(...)
56
+ Application::Writes::Put.new(
57
+ ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
58
+ bus: @bus, authorizer: @authorizer, hook_context: hook_context
59
+ ).call(...)
60
+ end
61
+
62
+ def delete(...)
63
+ Application::Writes::Delete.new(
64
+ ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
65
+ bus: @bus, authorizer: @authorizer, hook_context: hook_context
66
+ ).call(...)
67
+ end
68
+
69
+ def mv(...)
70
+ Application::Writes::Mv.new(
71
+ ctx: @ctx, manifest: @manifest, envelope_io: envelope_io,
72
+ bus: @bus, authorizer: @authorizer, hook_context: hook_context
73
+ ).call(...)
74
+ end
75
+
76
+ def accept(...)
77
+ Application::Writes::Accept.new(
78
+ ctx: @ctx, manifest: @manifest, file_store: @file_store, schemas: @schemas,
79
+ envelope_io: envelope_io, bus: @bus, authorizer: @authorizer, hook_context: hook_context
80
+ ).call(...)
81
+ end
82
+
83
+ def reject(...)
84
+ Application::Writes::Reject.new(
85
+ ctx: @ctx, manifest: @manifest, file_store: @file_store,
86
+ envelope_io: envelope_io, bus: @bus, authorizer: @authorizer, hook_context: hook_context
87
+ ).call(...)
88
+ end
89
+
90
+ def publish(...)
91
+ Application::Writes::Publish.new(
92
+ ctx: @ctx, manifest: @manifest, file_store: @file_store,
93
+ bus: @bus, root: @root, store: @store, hook_context: hook_context
94
+ ).call(...)
95
+ end
38
96
 
39
97
  # reads
40
- def get(...) = get_op.call(...)
41
- def get_or_refresh(...) = get_or_refresh_op.call(...)
42
- def list(...) = list_op.call(...)
43
- def where(...) = where_op.call(...)
44
- def uid(...) = uid_op.call(...)
45
- def schema_envelope(...) = schema_envelope_op.call(...)
46
- def deps(...) = deps_op.call(...)
47
- def rdeps(...) = rdeps_op.call(...)
48
- def published(...) = published_op.call(...)
49
- def stale(...) = stale_op.call(...)
50
- def audit(...) = audit_op.call(...)
51
- def blame(...) = blame_op.call(...)
52
- def policy_explain(...) = policy_explain_op.call(...)
53
- def freshness(...) = freshness_op.call(...)
54
- def validate_all(...) = validate_all_op.call(...)
98
+ def get(...)
99
+ Application::Reads::Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
100
+ end
101
+
102
+ def get_or_refresh(...)
103
+ Application::Reads::GetOrRefresh.new(
104
+ manifest: @manifest,
105
+ get: Application::Reads::Get.new(ctx: @ctx, manifest: @manifest, file_store: @file_store),
106
+ orchestrator: orchestrator,
107
+ ).call(...)
108
+ end
109
+
110
+ def list(...) = Application::Reads::List.new(manifest: @manifest).call(...)
111
+ def where(...) = Application::Reads::Where.new(manifest: @manifest).call(...)
112
+ def uid(...) = Application::Reads::Uid.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
113
+ def schema_envelope(...) = Application::Reads::SchemaEnvelope.new(manifest: @manifest, schemas: @schemas).call(...)
114
+ def deps(...) = Application::Reads::Deps.new(manifest: @manifest).call(...)
115
+ def rdeps(...) = Application::Reads::Rdeps.new(manifest: @manifest).call(...)
116
+ def published(...) = Application::Reads::Published.new(manifest: @manifest).call(...)
117
+ def stale(...) = Application::Reads::Stale.new(manifest: @manifest).call(...)
118
+ def audit(...) = Application::Reads::Audit.new(manifest: @manifest, root: @root).call(...)
119
+ def blame(...) = Application::Reads::Blame.new(manifest: @manifest, root: @root).call(...)
120
+ def policy_explain(...) = Application::Reads::PolicyExplain.new(manifest: @manifest).call(...)
121
+ def freshness(...) = Application::Reads::Freshness.new(ctx: @ctx, manifest: @manifest, file_store: @file_store).call(...)
122
+
123
+ def validate_all(...)
124
+ Application::Reads::ValidateAll.new(
125
+ ctx: @ctx, manifest: @manifest, file_store: @file_store, schemas: @schemas, audit_log: @audit_log,
126
+ ).call(...)
127
+ end
55
128
 
56
129
  # refresh
57
- def refresh(key) = refresh_worker_op.run(key)
58
- def refresh_all(**) = Application::Refresh::All.call(@ctx, **)
130
+ def refresh(key) = refresh_worker.run(key)
131
+
132
+ def refresh_all(**)
133
+ Application::Refresh::All.new(
134
+ ctx: @ctx, manifest: @manifest, envelope_io: envelope_io, bus: @bus,
135
+ store: @store, authorizer: @authorizer, hook_context: hook_context
136
+ ).call(**)
137
+ end
59
138
 
60
139
  private
61
140
 
62
141
  def envelope_io
63
142
  @envelope_io ||= Application::Writes::EnvelopeIO.new(
64
- file_store: @ctx.file_store,
65
- manifest: @ctx.manifest,
66
- schemas: @ctx.schemas,
67
- audit_log: @ctx.audit_log,
143
+ file_store: @file_store,
144
+ manifest: @manifest,
145
+ schemas: @schemas,
146
+ audit_log: @audit_log,
68
147
  ctx: @ctx,
69
148
  )
70
149
  end
71
150
 
72
- def put_op = @put_op ||= Application::Writes::Put.new(ctx: @ctx, envelope_io: envelope_io)
73
- def delete_op = @delete_op ||= Application::Writes::Delete.new(ctx: @ctx, envelope_io: envelope_io)
74
- def mv_op = @mv_op ||= Application::Writes::Mv.new(ctx: @ctx, envelope_io: envelope_io)
75
- def accept_op = @accept_op ||= Application::Writes::Accept.new(ctx: @ctx, envelope_io: envelope_io)
76
- def reject_op = @reject_op ||= Application::Writes::Reject.new(ctx: @ctx, envelope_io: envelope_io)
77
- def build_op = @build_op ||= Application::Writes::Build.new(ctx: @ctx)
78
- def publish_op = @publish_op ||= Application::Writes::Publish.new(ctx: @ctx)
79
-
80
- def get_op = @get_op ||= Application::Reads::Get.new(ctx: @ctx) # rubocop:disable Naming/AccessorMethodName
81
-
82
- def get_or_refresh_op # rubocop:disable Naming/AccessorMethodName
83
- @get_or_refresh_op ||= Application::Reads::GetOrRefresh.new(ctx: @ctx, get: get_op,
84
- orchestrator: orchestrator_op)
85
- end
86
-
87
- def list_op = @list_op ||= Application::Reads::List.new(ctx: @ctx)
88
- def where_op = @where_op ||= Application::Reads::Where.new(ctx: @ctx)
89
- def uid_op = @uid_op ||= Application::Reads::Uid.new(ctx: @ctx)
90
- def schema_envelope_op = @schema_envelope_op ||= Application::Reads::SchemaEnvelope.new(ctx: @ctx)
91
- def deps_op = @deps_op ||= Application::Reads::Deps.new(ctx: @ctx)
92
- def rdeps_op = @rdeps_op ||= Application::Reads::Rdeps.new(ctx: @ctx)
93
- def published_op = @published_op ||= Application::Reads::Published.new(ctx: @ctx)
94
- def stale_op = @stale_op ||= Application::Reads::Stale.new(ctx: @ctx)
95
- def audit_op = @audit_op ||= Application::Reads::Audit.new(ctx: @ctx)
96
- def blame_op = @blame_op ||= Application::Reads::Blame.new(ctx: @ctx)
97
- def policy_explain_op = @policy_explain_op ||= Application::Reads::PolicyExplain.new(ctx: @ctx)
98
- def freshness_op = @freshness_op ||= Application::Reads::Freshness.new(ctx: @ctx)
99
- def validate_all_op = @validate_all_op ||= Application::Reads::ValidateAll.new(ctx: @ctx)
100
-
101
- def refresh_worker_op = @refresh_worker_op ||= Application::Refresh::Worker.new(ctx: @ctx, envelope_io: envelope_io)
102
-
103
- def orchestrator_op
104
- @orchestrator_op ||= Application::Refresh::Orchestrator.new(
105
- worker: refresh_worker_op,
106
- store_root: @ctx.store.root,
107
- store: @ctx.store,
108
- role: @ctx.role,
151
+ def refresh_worker
152
+ @refresh_worker ||= Application::Refresh::Worker.new(
153
+ ctx: @ctx, manifest: @manifest, envelope_io: envelope_io, bus: @bus,
154
+ store: @store, authorizer: @authorizer, hook_context: hook_context
155
+ )
156
+ end
157
+
158
+ def orchestrator
159
+ @orchestrator ||= Application::Refresh::Orchestrator.new(
160
+ worker: refresh_worker,
161
+ store_root: @root,
162
+ bus: @bus,
163
+ store: @store,
164
+ ctx: @ctx,
165
+ hook_context: hook_context,
109
166
  )
110
167
  end
111
168
  end
@@ -24,7 +24,7 @@ module Textus
24
24
  def self.diff(store, name:)
25
25
  schema = load_schema(store, name)
26
26
  drift = []
27
- store.manifest.enumerate.each do |row|
27
+ store.manifest.resolver.enumerate.each do |row|
28
28
  env = Textus::Operations.for(store).get(row[:key])
29
29
  begin
30
30
  schema.validate!(env.meta)
@@ -51,7 +51,7 @@ module Textus
51
51
 
52
52
  ops = Textus::Operations.for(store, role: "human")
53
53
  touched = []
54
- store.manifest.enumerate.each do |row|
54
+ store.manifest.resolver.enumerate.each do |row|
55
55
  env = ops.get(row[:key])
56
56
  meta = env.meta.dup
57
57
  changed = false
data/lib/textus/store.rb CHANGED
@@ -2,7 +2,7 @@ require "fileutils"
2
2
 
3
3
  module Textus
4
4
  class Store
5
- attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus, :registry
5
+ attr_reader :root, :manifest, :schemas, :file_store, :audit_log, :bus
6
6
 
7
7
  def self.discover(start_dir = Dir.pwd, root: nil)
8
8
  explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
@@ -34,12 +34,12 @@ module Textus
34
34
  @schemas = Schemas.new(File.join(@root, "schemas"))
35
35
  @file_store = Infra::Storage::FileStore.new
36
36
  @audit_log = Infra::AuditLog.new(@root)
37
- @bus = Hooks::Dispatcher.new
38
- @registry = Hooks::Registry.new(dispatcher: @bus)
37
+ @bus = Hooks::Bus.new
39
38
  Infra::AuditSubscriber.new(@audit_log).attach(@bus)
40
- Hooks::Builtin.register_all(@registry)
41
- Hooks::Loader.new(registry: @registry).load_dir(File.join(@root, "hooks"))
42
- @bus.publish(:store_loaded, store: Application::Context.system(self))
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)
43
43
  end
44
44
  end
45
45
  end
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.18.0"
2
+ VERSION = "0.20.0"
3
3
  PROTOCOL = "textus/3"
4
4
  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.18.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
@@ -129,10 +133,12 @@ files:
129
133
  - lib/textus/application/refresh/all.rb
130
134
  - lib/textus/application/refresh/orchestrator.rb
131
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
132
138
  - lib/textus/application/writes/accept.rb
133
- - lib/textus/application/writes/build.rb
134
139
  - lib/textus/application/writes/delete.rb
135
140
  - lib/textus/application/writes/envelope_io.rb
141
+ - lib/textus/application/writes/materializer.rb
136
142
  - lib/textus/application/writes/mv.rb
137
143
  - lib/textus/application/writes/publish.rb
138
144
  - lib/textus/application/writes/put.rb
@@ -181,7 +187,6 @@ files:
181
187
  - lib/textus/cli/verb/schema_migrate.rb
182
188
  - lib/textus/cli/verb/uid.rb
183
189
  - lib/textus/cli/verb/where.rb
184
- - lib/textus/dependencies.rb
185
190
  - lib/textus/doctor.rb
186
191
  - lib/textus/doctor/check.rb
187
192
  - lib/textus/doctor/check/audit_log.rb
@@ -200,6 +205,7 @@ files:
200
205
  - lib/textus/doctor/check/templates.rb
201
206
  - lib/textus/doctor/check/unowned_schema_fields.rb
202
207
  - lib/textus/domain/action.rb
208
+ - lib/textus/domain/authorizer.rb
203
209
  - lib/textus/domain/freshness.rb
204
210
  - lib/textus/domain/freshness/evaluator.rb
205
211
  - lib/textus/domain/freshness/policy.rb
@@ -209,10 +215,7 @@ files:
209
215
  - lib/textus/domain/policy.rb
210
216
  - lib/textus/domain/policy/handler_allowlist.rb
211
217
  - lib/textus/domain/policy/matcher.rb
212
- - lib/textus/domain/policy/predicates/human_accept.rb
213
- - lib/textus/domain/policy/predicates/schema_valid.rb
214
218
  - lib/textus/domain/policy/promote.rb
215
- - lib/textus/domain/policy/promotion.rb
216
219
  - lib/textus/domain/policy/refresh.rb
217
220
  - lib/textus/domain/sentinel.rb
218
221
  - lib/textus/domain/staleness.rb
@@ -228,9 +231,10 @@ files:
228
231
  - lib/textus/errors.rb
229
232
  - lib/textus/etag.rb
230
233
  - lib/textus/hooks/builtin.rb
231
- - lib/textus/hooks/dispatcher.rb
234
+ - lib/textus/hooks/bus.rb
235
+ - lib/textus/hooks/context.rb
236
+ - lib/textus/hooks/fire_report.rb
232
237
  - lib/textus/hooks/loader.rb
233
- - lib/textus/hooks/registry.rb
234
238
  - lib/textus/infra/audit_log.rb
235
239
  - lib/textus/infra/audit_subscriber.rb
236
240
  - lib/textus/infra/build_lock.rb
@@ -247,6 +251,11 @@ files:
247
251
  - lib/textus/key/path.rb
248
252
  - lib/textus/manifest.rb
249
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
250
259
  - lib/textus/manifest/entry/parser.rb
251
260
  - lib/textus/manifest/entry/validators.rb
252
261
  - lib/textus/manifest/entry/validators/events.rb
@@ -255,13 +264,11 @@ files:
255
264
  - lib/textus/manifest/entry/validators/inject_intro.rb
256
265
  - lib/textus/manifest/entry/validators/publish_each.rb
257
266
  - lib/textus/manifest/resolution.rb
267
+ - lib/textus/manifest/resolver.rb
258
268
  - lib/textus/manifest/rules.rb
259
269
  - lib/textus/manifest/schema.rb
260
- - lib/textus/migrate_keys.rb
261
270
  - lib/textus/mustache.rb
262
271
  - lib/textus/operations.rb
263
- - lib/textus/projection.rb
264
- - lib/textus/refresh.rb
265
272
  - lib/textus/role.rb
266
273
  - lib/textus/schema.rb
267
274
  - lib/textus/schema/tools.rb
@@ -1,78 +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:)
15
- @ctx = ctx
16
- end
17
-
18
- def call(prefix: nil)
19
- built = manifest.entries.filter_map do |mentry|
20
- next unless mentry.in_generator_zone?
21
- next unless mentry.projection || mentry.template
22
- next if prefix && !mentry.key.start_with?(prefix)
23
-
24
- materialize(mentry)
25
- end
26
- { "protocol" => Textus::PROTOCOL, "built" => built }
27
- end
28
-
29
- private
30
-
31
- def store = @ctx.store
32
- def manifest = store.manifest
33
- def root = store.root
34
-
35
- def materialize(mentry)
36
- target_path = Builder::Pipeline.run(
37
- store: store,
38
- mentry: mentry,
39
- template_loader: ->(name) { read_template(name) },
40
- )
41
- publish_and_fire(mentry, target_path)
42
- { "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
43
- end
44
-
45
- def read_template(name)
46
- tpl_path = File.join(root, "templates", name)
47
- raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
48
-
49
- File.read(tpl_path)
50
- end
51
-
52
- def publish_and_fire(mentry, target_path)
53
- envelope = Textus::Application::Reads::Get.new(ctx: @ctx).call(mentry.key)
54
- repo_root = File.dirname(root)
55
-
56
- mentry.publish_to.each do |rel|
57
- target_abs = File.join(repo_root, rel)
58
- Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: root)
59
- publish_event(:file_published,
60
- key: mentry.key,
61
- envelope: envelope,
62
- source: target_path,
63
- target: target_abs)
64
- end
65
-
66
- publish_event(:build_completed,
67
- key: mentry.key,
68
- envelope: envelope,
69
- sources: Array(mentry.projection&.fetch("select", nil)).compact)
70
- end
71
-
72
- def publish_event(event, **payload)
73
- @ctx.bus.publish(event, store: @ctx.with_role(@ctx.role), correlation_id: @ctx.correlation_id, **payload)
74
- end
75
- end
76
- end
77
- end
78
- 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