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,18 +1,10 @@
1
1
  require "yaml"
2
2
  require_relative "manifest/schema"
3
- require_relative "manifest/resolution"
3
+ require_relative "manifest/resolver"
4
+ require_relative "manifest/role_kinds"
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
@@ -38,6 +30,26 @@ module Textus
38
30
  )
39
31
  end
40
32
 
33
+ def role_mapping
34
+ @role_mapping ||= RoleKinds.resolve(@raw["roles"])
35
+ end
36
+
37
+ def role_kind(name)
38
+ role_mapping[name]
39
+ end
40
+
41
+ def roles_with_kind(kind)
42
+ role_mapping.each_with_object([]) { |(name, k), acc| acc << name if k == kind }
43
+ end
44
+
45
+ def zone_kinds(zone_name)
46
+ @zone_kinds_cache ||= {}
47
+ @zone_kinds_cache[zone_name] ||= zone_writers(zone_name).each_with_object(Set.new) do |w, acc|
48
+ k = role_kind(w)
49
+ acc << k if k
50
+ end.freeze
51
+ end
52
+
41
53
  def self.parse(yaml_text, root: ".")
42
54
  raw = YAML.safe_load(yaml_text, aliases: false)
43
55
  check_version!(raw, "<string>")
@@ -59,7 +71,6 @@ module Textus
59
71
  raise BadFrontmatter.new(
60
72
  source,
61
73
  "unsupported manifest version #{raw["version"].inspect}; expected #{PROTOCOL.inspect}",
62
- hint: version_hint_for(raw["version"]),
63
74
  )
64
75
  end
65
76
  private_class_method :check_version!
@@ -87,53 +98,8 @@ module Textus
87
98
  rules.for(key)
88
99
  end
89
100
 
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] }
101
+ def resolver
102
+ @resolver ||= Resolver.new(self)
137
103
  end
138
104
 
139
105
  def validate_key!(key)
@@ -144,51 +110,8 @@ module Textus
144
110
 
145
111
  private
146
112
 
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
113
  def validate_declared_keys!
183
114
  @entries.each { |e| validate_key!(e.key) }
184
115
  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
116
  end
194
117
  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)
@@ -49,9 +49,16 @@ module Textus
49
49
  end
50
50
  raise UsageError.new("schema migrate needs --rename=OLD:NEW or schema.evolution.migrate_from") if renames.empty?
51
51
 
52
- ops = Textus::Operations.for(store, role: "human")
52
+ authority = store.manifest.roles_with_kind(:accept_authority).first
53
+ if authority.nil?
54
+ raise UsageError.new(
55
+ "schema migrate requires a role with kind :accept_authority in the manifest; " \
56
+ "none declared (add e.g. `- { name: owner, kind: accept_authority }` to roles:)",
57
+ )
58
+ end
59
+ ops = Textus::Operations.for(store, role: authority)
53
60
  touched = []
54
- store.manifest.enumerate.each do |row|
61
+ store.manifest.resolver.enumerate.each do |row|
55
62
  env = ops.get(row[:key])
56
63
  meta = env.meta.dup
57
64
  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.2"
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.2
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/accept_authority_signed.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
@@ -130,9 +134,10 @@ files:
130
134
  - lib/textus/application/refresh/orchestrator.rb
131
135
  - lib/textus/application/refresh/worker.rb
132
136
  - lib/textus/application/writes/accept.rb
133
- - lib/textus/application/writes/build.rb
137
+ - lib/textus/application/writes/authority_gate.rb
134
138
  - lib/textus/application/writes/delete.rb
135
139
  - lib/textus/application/writes/envelope_io.rb
140
+ - lib/textus/application/writes/materializer.rb
136
141
  - lib/textus/application/writes/mv.rb
137
142
  - lib/textus/application/writes/publish.rb
138
143
  - lib/textus/application/writes/put.rb
@@ -164,7 +169,6 @@ files:
164
169
  - lib/textus/cli/verb/hooks.rb
165
170
  - lib/textus/cli/verb/init.rb
166
171
  - lib/textus/cli/verb/intro.rb
167
- - lib/textus/cli/verb/key_normalize.rb
168
172
  - lib/textus/cli/verb/list.rb
169
173
  - lib/textus/cli/verb/mv.rb
170
174
  - lib/textus/cli/verb/published.rb
@@ -181,7 +185,6 @@ files:
181
185
  - lib/textus/cli/verb/schema_migrate.rb
182
186
  - lib/textus/cli/verb/uid.rb
183
187
  - lib/textus/cli/verb/where.rb
184
- - lib/textus/dependencies.rb
185
188
  - lib/textus/doctor.rb
186
189
  - lib/textus/doctor/check.rb
187
190
  - lib/textus/doctor/check/audit_log.rb
@@ -200,19 +203,16 @@ files:
200
203
  - lib/textus/doctor/check/templates.rb
201
204
  - lib/textus/doctor/check/unowned_schema_fields.rb
202
205
  - lib/textus/domain/action.rb
206
+ - lib/textus/domain/authorizer.rb
203
207
  - lib/textus/domain/freshness.rb
204
208
  - lib/textus/domain/freshness/evaluator.rb
205
209
  - lib/textus/domain/freshness/policy.rb
206
210
  - lib/textus/domain/freshness/verdict.rb
207
211
  - lib/textus/domain/outcome.rb
208
212
  - lib/textus/domain/permission.rb
209
- - lib/textus/domain/policy.rb
210
213
  - lib/textus/domain/policy/handler_allowlist.rb
211
214
  - 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
215
  - lib/textus/domain/policy/promote.rb
215
- - lib/textus/domain/policy/promotion.rb
216
216
  - lib/textus/domain/policy/refresh.rb
217
217
  - lib/textus/domain/sentinel.rb
218
218
  - lib/textus/domain/staleness.rb
@@ -228,9 +228,10 @@ files:
228
228
  - lib/textus/errors.rb
229
229
  - lib/textus/etag.rb
230
230
  - lib/textus/hooks/builtin.rb
231
- - lib/textus/hooks/dispatcher.rb
231
+ - lib/textus/hooks/bus.rb
232
+ - lib/textus/hooks/context.rb
233
+ - lib/textus/hooks/fire_report.rb
232
234
  - lib/textus/hooks/loader.rb
233
- - lib/textus/hooks/registry.rb
234
235
  - lib/textus/infra/audit_log.rb
235
236
  - lib/textus/infra/audit_subscriber.rb
236
237
  - lib/textus/infra/build_lock.rb
@@ -247,6 +248,11 @@ files:
247
248
  - lib/textus/key/path.rb
248
249
  - lib/textus/manifest.rb
249
250
  - lib/textus/manifest/entry.rb
251
+ - lib/textus/manifest/entry/base.rb
252
+ - lib/textus/manifest/entry/derived.rb
253
+ - lib/textus/manifest/entry/intake.rb
254
+ - lib/textus/manifest/entry/leaf.rb
255
+ - lib/textus/manifest/entry/nested.rb
250
256
  - lib/textus/manifest/entry/parser.rb
251
257
  - lib/textus/manifest/entry/validators.rb
252
258
  - lib/textus/manifest/entry/validators/events.rb
@@ -254,14 +260,12 @@ files:
254
260
  - lib/textus/manifest/entry/validators/index_filename.rb
255
261
  - lib/textus/manifest/entry/validators/inject_intro.rb
256
262
  - lib/textus/manifest/entry/validators/publish_each.rb
257
- - lib/textus/manifest/resolution.rb
263
+ - lib/textus/manifest/resolver.rb
264
+ - lib/textus/manifest/role_kinds.rb
258
265
  - lib/textus/manifest/rules.rb
259
266
  - lib/textus/manifest/schema.rb
260
- - lib/textus/migrate_keys.rb
261
267
  - lib/textus/mustache.rb
262
268
  - lib/textus/operations.rb
263
- - lib/textus/projection.rb
264
- - lib/textus/refresh.rb
265
269
  - lib/textus/role.rb
266
270
  - lib/textus/schema.rb
267
271
  - 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,19 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class KeyNormalize < Verb
5
- command_name "normalize"
6
- parent_group Group::Key
7
-
8
- option :write, "--write"
9
- option :dry_run, "--dry-run"
10
-
11
- def call(store)
12
- effective_write = write && !dry_run
13
- res = Textus::MigrateKeys.run(store, write: effective_write || false)
14
- emit(res, exit_code: res["ok"] ? 0 : 1)
15
- end
16
- end
17
- end
18
- end
19
- end