textus 0.8.1 → 0.10.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +329 -0
  3. data/README.md +50 -22
  4. data/SPEC.md +194 -63
  5. data/docs/architecture.md +22 -4
  6. data/docs/conventions.md +24 -17
  7. data/lib/textus/application/context.rb +44 -0
  8. data/lib/textus/application/reads/audit.rb +69 -0
  9. data/lib/textus/application/reads/blame.rb +79 -0
  10. data/lib/textus/application/reads/freshness.rb +77 -0
  11. data/lib/textus/application/reads/get.rb +62 -0
  12. data/lib/textus/application/reads/policy_explain.rb +39 -0
  13. data/lib/textus/application/refresh/all.rb +41 -0
  14. data/lib/textus/application/refresh/orchestrator.rb +69 -0
  15. data/lib/textus/application/refresh/worker.rb +79 -0
  16. data/lib/textus/application/writes/accept.rb +44 -0
  17. data/lib/textus/application/writes/build.rb +116 -0
  18. data/lib/textus/application/writes/delete.rb +36 -0
  19. data/lib/textus/application/writes/publish.rb +25 -0
  20. data/lib/textus/application/writes/put.rb +43 -0
  21. data/lib/textus/builder/pipeline.rb +1 -1
  22. data/lib/textus/builder/renderer/json.rb +1 -1
  23. data/lib/textus/builder/renderer/markdown.rb +1 -1
  24. data/lib/textus/builder/renderer/text.rb +1 -1
  25. data/lib/textus/builder/renderer/yaml.rb +1 -1
  26. data/lib/textus/builder/renderer.rb +1 -1
  27. data/lib/textus/cli/group/policy.rb +11 -0
  28. data/lib/textus/cli/verb/accept.rb +2 -2
  29. data/lib/textus/cli/verb/audit.rb +30 -0
  30. data/lib/textus/cli/verb/blame.rb +16 -0
  31. data/lib/textus/cli/verb/build.rb +2 -1
  32. data/lib/textus/cli/verb/delete.rb +2 -2
  33. data/lib/textus/cli/verb/freshness.rb +16 -0
  34. data/lib/textus/cli/verb/get.rb +7 -1
  35. data/lib/textus/cli/verb/hook_run.rb +4 -4
  36. data/lib/textus/cli/verb/mv.rb +1 -2
  37. data/lib/textus/cli/verb/policy_explain.rb +14 -0
  38. data/lib/textus/cli/verb/policy_list.rb +25 -0
  39. data/lib/textus/cli/verb/put.rb +10 -8
  40. data/lib/textus/cli/verb/refresh.rb +2 -2
  41. data/lib/textus/cli/verb/refresh_stale.rb +18 -0
  42. data/lib/textus/cli/verb/reject.rb +14 -0
  43. data/lib/textus/cli/verb.rb +14 -0
  44. data/lib/textus/cli.rb +16 -2
  45. data/lib/textus/composition.rb +72 -0
  46. data/lib/textus/doctor/check/handler_allowlist.rb +33 -0
  47. data/lib/textus/doctor/check/intake_registration.rb +46 -0
  48. data/lib/textus/doctor/check/legacy_intake_fields.rb +57 -0
  49. data/lib/textus/doctor/check/policy_ambiguity.rb +49 -0
  50. data/lib/textus/doctor.rb +7 -1
  51. data/lib/textus/domain/action.rb +9 -0
  52. data/lib/textus/domain/freshness/evaluator.rb +30 -0
  53. data/lib/textus/domain/freshness/policy.rb +18 -0
  54. data/lib/textus/domain/freshness/verdict.rb +12 -0
  55. data/lib/textus/domain/outcome.rb +10 -0
  56. data/lib/textus/domain/permission.rb +15 -0
  57. data/lib/textus/domain/policy/handler_allowlist.rb +17 -0
  58. data/lib/textus/domain/policy/matcher.rb +51 -0
  59. data/lib/textus/domain/policy/promote.rb +24 -0
  60. data/lib/textus/domain/policy/refresh.rb +48 -0
  61. data/lib/textus/domain/policy.rb +7 -0
  62. data/lib/textus/hooks/builtin.rb +5 -5
  63. data/lib/textus/hooks/dispatcher.rb +15 -1
  64. data/lib/textus/hooks/dsl.rb +18 -0
  65. data/lib/textus/hooks/registry.rb +12 -5
  66. data/lib/textus/infra/clock.rb +9 -0
  67. data/lib/textus/infra/event_bus.rb +27 -0
  68. data/lib/textus/infra/publisher.rb +73 -0
  69. data/lib/textus/infra/refresh/detached.rb +38 -0
  70. data/lib/textus/infra/refresh/lock.rb +44 -0
  71. data/lib/textus/init.rb +71 -28
  72. data/lib/textus/intro.rb +17 -14
  73. data/lib/textus/manifest/entry.rb +39 -13
  74. data/lib/textus/manifest/policies.rb +83 -0
  75. data/lib/textus/manifest.rb +30 -11
  76. data/lib/textus/projection.rb +1 -1
  77. data/lib/textus/proposal.rb +4 -21
  78. data/lib/textus/refresh.rb +9 -45
  79. data/lib/textus/store/mover.rb +14 -9
  80. data/lib/textus/store/reader.rb +10 -8
  81. data/lib/textus/store/staleness.rb +5 -17
  82. data/lib/textus/store/validator.rb +46 -20
  83. data/lib/textus/store/writer.rb +51 -14
  84. data/lib/textus/store.rb +30 -10
  85. data/lib/textus/version.rb +1 -1
  86. data/lib/textus.rb +1 -0
  87. metadata +46 -5
  88. data/lib/textus/builder.rb +0 -86
  89. data/lib/textus/cli/verb/stale.rb +0 -14
  90. data/lib/textus/publisher.rb +0 -71
  91. data/lib/textus/store/view.rb +0 -29
@@ -4,14 +4,15 @@ module Textus
4
4
  class Store
5
5
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
6
6
  class Mover
7
- def initialize(reader:, writer:, manifest:, audit_log:)
7
+ def initialize(store:, reader:, writer:, manifest:, audit_log:)
8
+ @store = store
8
9
  @reader = reader
9
10
  @writer = writer
10
11
  @manifest = manifest
11
12
  @audit_log = audit_log
12
13
  end
13
14
 
14
- def call(old_key, new_key, as: Role::DEFAULT, dry_run: false)
15
+ def call(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
15
16
  @manifest.validate_key!(old_key)
16
17
  @manifest.validate_key!(new_key)
17
18
  raise UsageError.new("mv: old and new keys are identical") if old_key == new_key
@@ -69,23 +70,27 @@ module Textus
69
70
  rewrite_name_for_mv!(new_mentry, new_path, new_key)
70
71
  etag_after = Etag.for_file(new_path)
71
72
 
73
+ extras = {
74
+ "from_key" => old_key, "to_key" => new_key,
75
+ "from_path" => old_path, "to_path" => new_path,
76
+ "uid" => current_uid
77
+ }
78
+ extras["correlation_id"] = correlation_id if correlation_id
79
+
72
80
  @audit_log.append(
73
81
  role: as, verb: "mv", key: new_key,
74
82
  etag_before: etag_before, etag_after: etag_after,
75
- extras: {
76
- "from_key" => old_key, "to_key" => new_key,
77
- "from_path" => old_path, "to_path" => new_path,
78
- "uid" => current_uid
79
- }
83
+ extras: extras
80
84
  )
81
85
 
82
- env = @reader.get(new_key)
86
+ new_envelope = @reader.get(new_key)
87
+ @store.fire_event(:mv, key: new_key, from_key: old_key, to_key: new_key, envelope: new_envelope)
83
88
  {
84
89
  "protocol" => PROTOCOL, "ok" => true,
85
90
  "from_key" => old_key, "to_key" => new_key,
86
91
  "from_path" => old_path, "to_path" => new_path,
87
92
  "uid" => current_uid,
88
- "envelope" => env
93
+ "envelope" => new_envelope
89
94
  }
90
95
  end
91
96
 
@@ -7,20 +7,22 @@ module Textus
7
7
  end
8
8
 
9
9
  def get(key)
10
+ read_raw_envelope(key) || raise(UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)))
11
+ end
12
+
13
+ # Reads the current on-disk state of key as a bare envelope, skipping
14
+ # freshness annotation to avoid recursion. Used by Freshness.refresh_sync
15
+ # after a sync refresh completes.
16
+ def read_raw_envelope(key)
10
17
  mentry, path, = @manifest.resolve(key)
11
- raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
18
+ return nil unless File.exist?(path)
12
19
 
13
20
  raw = File.binread(path)
14
21
  parsed = Entry.for_format(mentry.format).parse(raw, path: path)
15
- meta = parsed["_meta"]
16
- content = parsed["content"]
17
- @store.writer.enforce_name_match!(path, meta, mentry.format)
18
- schema = @store.schema_for(mentry.schema)
19
- Entry.for_format(mentry.format).validate_against(schema, parsed) if schema
20
22
  Envelope.build(
21
23
  key: key, mentry: mentry, path: path,
22
- meta: meta, body: parsed["body"],
23
- etag: Etag.for_bytes(raw), content: content
24
+ meta: parsed["_meta"], body: parsed["body"],
25
+ etag: Etag.for_bytes(raw), content: parsed["content"]
24
26
  )
25
27
  end
26
28
 
@@ -11,7 +11,7 @@ module Textus
11
11
  def call(prefix: nil, zone: nil)
12
12
  out = []
13
13
  @manifest.entries.each do |mentry|
14
- next unless mentry.zone == "derived"
14
+ next unless mentry.in_generator_zone?
15
15
  next if zone && mentry.zone != zone
16
16
 
17
17
  gen = mentry.generator
@@ -47,11 +47,12 @@ module Textus
47
47
  end
48
48
 
49
49
  @manifest.entries.each do |mentry|
50
- next unless mentry.fetch
50
+ next unless mentry.intake_handler
51
51
  next if zone && mentry.zone != zone
52
52
  next if prefix && !(mentry.key == prefix || mentry.key.start_with?("#{prefix}."))
53
53
 
54
- ttl = parse_ttl(mentry.ttl)
54
+ policy_set = @manifest.policies_for(mentry.key)
55
+ ttl = policy_set.refresh&.ttl_seconds
55
56
  next unless ttl
56
57
 
57
58
  path = Textus::Key::Path.resolve(@manifest, mentry)
@@ -102,21 +103,8 @@ module Textus
102
103
  nil
103
104
  end
104
105
 
105
- def parse_ttl(s)
106
- return nil unless s
107
-
108
- m = s.to_s.match(/\A(\d+)([smhd])\z/) or return nil
109
- n = m[1].to_i
110
- case m[2]
111
- when "s" then n
112
- when "m" then n * 60
113
- when "h" then n * 3600
114
- when "d" then n * 86_400
115
- end
116
- end
117
-
118
106
  def intake_stale_row(mentry, path, reason)
119
- { "key" => mentry.key, "path" => path, "fetch" => mentry.fetch, "reason" => reason }
107
+ { "key" => mentry.key, "path" => path, "handler" => mentry.intake_handler, "reason" => reason }
120
108
  end
121
109
 
122
110
  def stale_row(mentry, path, reason)
@@ -10,14 +10,30 @@ module Textus
10
10
 
11
11
  def call
12
12
  violations = []
13
+ check_content_violations(violations)
14
+ check_role_authority_violations(violations)
15
+ { "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
16
+ end
17
+
18
+ private
19
+
20
+ def check_content_violations(violations)
13
21
  @manifest.enumerate.each do |row|
22
+ key = row[:key]
23
+ mentry = row[:manifest_entry]
24
+ env = fetch_envelope(key, violations) or next
25
+ schema = mentry.schema && @schema_for.call(mentry.schema)
26
+ next unless schema
27
+
14
28
  begin
15
- @reader.get(row[:key])
29
+ validate_schema!(schema, env, mentry.format)
16
30
  rescue Textus::Error => e
17
- violations << { "key" => row[:key], "code" => e.code, "message" => e.message }
31
+ violations << { "key" => key, "code" => e.code, "message" => e.message }
18
32
  end
19
33
  end
34
+ end
20
35
 
36
+ def check_role_authority_violations(violations)
21
37
  @manifest.enumerate.each do |row|
22
38
  mentry = row[:manifest_entry]
23
39
  next unless mentry.schema
@@ -30,26 +46,36 @@ module Textus
30
46
  rescue StandardError
31
47
  next
32
48
  end
33
- last_writer = @audit_log.last_writer_for(row[:key])
34
- next if last_writer.nil?
35
-
36
- env["_meta"].each_key do |field|
37
- owner = schema.maintained_by(field)
38
- next if owner.nil?
39
- next if last_writer == owner
40
- next if last_writer == "human"
41
-
42
- violations << {
43
- "key" => row[:key],
44
- "code" => "role_authority",
45
- "field" => field,
46
- "expected" => owner,
47
- "last_writer" => last_writer,
48
- }
49
- end
49
+ append_authority_violations(violations, row[:key], env, schema)
50
50
  end
51
+ end
51
52
 
52
- { "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
53
+ def append_authority_violations(violations, key, env, schema)
54
+ last_writer = @audit_log.last_writer_for(key)
55
+ return if last_writer.nil?
56
+
57
+ env["_meta"].each_key do |field|
58
+ owner = schema.maintained_by(field)
59
+ next if owner.nil? || last_writer == owner || last_writer == "human"
60
+
61
+ violations << { "key" => key, "code" => "role_authority",
62
+ "field" => field, "expected" => owner, "last_writer" => last_writer }
63
+ end
64
+ end
65
+
66
+ def fetch_envelope(key, violations)
67
+ @reader.get(key)
68
+ rescue Textus::Error => e
69
+ violations << { "key" => key, "code" => e.code, "message" => e.message }
70
+ nil
71
+ end
72
+
73
+ def validate_schema!(schema, envelope, format)
74
+ payload = case format
75
+ when "json", "yaml" then envelope["content"] || {}
76
+ else envelope["_meta"] || {}
77
+ end
78
+ schema.validate!(payload)
53
79
  end
54
80
  end
55
81
  end
@@ -10,11 +10,19 @@ module Textus
10
10
  @reader = store.reader
11
11
  end
12
12
 
13
+ # Backward-compat shim — orchestration now lives in Application::Writes::Put.
13
14
  def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
14
- @manifest.validate_key!(key)
15
- mentry, path, = @manifest.resolve(key)
16
- writers = @manifest.zone_writers(mentry.zone)
17
- raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
15
+ ctx = Textus::Application::Context.new(store: @store, role: as)
16
+ Textus::Application::Writes::Put.new(ctx: ctx, bus: @store.bus).call(
17
+ key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
18
+ )
19
+ end
20
+
21
+ # Pure I/O: validate, serialize, etag-check, write to disk, audit. No
22
+ # permission check and no event firing — those are handled by the caller
23
+ # (Application::Writes::Put).
24
+ def write_envelope_to_disk(key, mentry:, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, correlation_id: nil)
25
+ _, path, = @manifest.resolve(key)
18
26
 
19
27
  meta ||= {}
20
28
  strategy = Entry.for_format(mentry.format)
@@ -43,13 +51,15 @@ module Textus
43
51
  FileUtils.mkdir_p(File.dirname(path))
44
52
  File.binwrite(path, bytes)
45
53
  etag_after = Etag.for_bytes(bytes)
46
- @store.audit_log.append(role: as, verb: "put", key: key, etag_before: etag_before, etag_after: etag_after)
47
- envelope = Envelope.build(
54
+ @store.audit_log.append(
55
+ role: as, verb: "put", key: key,
56
+ etag_before: etag_before, etag_after: etag_after,
57
+ extras: correlation_id ? { "correlation_id" => correlation_id } : nil
58
+ )
59
+ Envelope.build(
48
60
  key: key, mentry: mentry, path: path,
49
61
  meta: eff_meta, body: eff_body, etag: etag_after, content: eff_content
50
62
  )
51
- @store.fire_event(:put, key: key, envelope: envelope) unless suppress_events
52
- envelope
53
63
  end
54
64
 
55
65
  def existing_uid_for(mentry, path)
@@ -108,24 +118,51 @@ module Textus
108
118
  end
109
119
  end
110
120
 
121
+ # Backward-compat shim — orchestration now lives in Application::Writes::Delete.
111
122
  def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
112
- mentry, path, = @manifest.resolve(key)
113
- writers = @manifest.zone_writers(mentry.zone)
114
- raise WriteForbidden.new(key, mentry.zone, writers: writers) unless writers.include?(as)
123
+ ctx = Textus::Application::Context.new(store: @store, role: as)
124
+ Textus::Application::Writes::Delete.new(ctx: ctx, bus: @store.bus).call(
125
+ key, if_etag: if_etag, suppress_events: suppress_events
126
+ )
127
+ end
128
+
129
+ # Pure I/O: resolve path, validate etag, delete from disk, audit. No
130
+ # permission check and no event firing — those are handled by the caller
131
+ # (Application::Writes::Delete).
132
+ def delete_envelope_from_disk(key, if_etag: nil, as: Role::DEFAULT, correlation_id: nil)
133
+ _, path, = @manifest.resolve(key)
115
134
  raise UnknownKey.new(key, suggestions: @manifest.suggestions_for(key)) unless File.exist?(path)
116
135
 
117
136
  etag_before = Etag.for_file(path)
118
137
  raise EtagMismatch.new(key, if_etag, etag_before) if if_etag && if_etag != etag_before
119
138
 
120
139
  File.delete(path)
121
- @store.audit_log.append(role: as, verb: "delete", key: key, etag_before: etag_before, etag_after: nil)
122
- @store.fire_event(:delete, key: key) unless suppress_events
123
- { "protocol" => PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
140
+ @store.audit_log.append(
141
+ role: as, verb: "delete", key: key,
142
+ etag_before: etag_before, etag_after: nil,
143
+ extras: correlation_id ? { "correlation_id" => correlation_id } : nil
144
+ )
124
145
  end
125
146
 
126
147
  def accept(key, as:)
127
148
  Proposal.accept(@store, key, as: as)
128
149
  end
150
+
151
+ def reject(pending_key, as: Role::DEFAULT)
152
+ raise ProposalError.new("only human role can reject proposals; got '#{as}'") unless as == "human"
153
+
154
+ mentry, = @store.manifest.resolve(pending_key)
155
+ raise ProposalError.new("reject: '#{pending_key}' is not in a proposal zone (zone=#{mentry.zone})") unless mentry.in_proposal_zone?
156
+
157
+ env = @store.get(pending_key)
158
+ proposal = env.dig("_meta", "proposal") or
159
+ raise ProposalError.new("entry has no proposal block: #{pending_key}")
160
+ target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
161
+
162
+ delete(pending_key, as: as)
163
+ @store.fire_event(:reject, key: pending_key, target_key: target_key)
164
+ { "protocol" => PROTOCOL, "rejected" => pending_key, "target_key" => target_key }
165
+ end
129
166
  end
130
167
  # rubocop:enable Metrics/ParameterLists
131
168
  end
data/lib/textus/store.rb CHANGED
@@ -45,6 +45,7 @@ module Textus
45
45
  load_hooks
46
46
  @reader = Reader.new(self)
47
47
  @writer = Writer.new(self)
48
+ fire_event(:loaded)
48
49
  end
49
50
 
50
51
  def load_hooks
@@ -53,7 +54,7 @@ module Textus
53
54
  dir = File.join(@root, "hooks")
54
55
  return unless File.directory?(dir)
55
56
 
56
- Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
57
+ Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
57
58
  begin
58
59
  load(f)
59
60
  rescue StandardError, ScriptError => e
@@ -74,24 +75,43 @@ module Textus
74
75
  end
75
76
  end
76
77
 
77
- def get(key)
78
- @reader.get(key)
78
+ def get(key, as: Textus::Role::DEFAULT)
79
+ ctx = Textus::Composition.context(self, role: as)
80
+ result = Textus::Composition.reads_get(ctx).call(key)
81
+ raise UnknownKey.new(key, suggestions: manifest.suggestions_for(key)) if result.nil?
82
+
83
+ result
79
84
  end
80
85
 
81
86
  def where(key) = @reader.where(key)
82
87
  def list(**) = @reader.list(**)
83
88
  def schema_envelope(key) = @reader.schema_envelope(key)
84
89
 
85
- def put(...) = @writer.put(...)
90
+ # rubocop:disable Metrics/ParameterLists
91
+ def put(key, meta: nil, body: nil, content: nil, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
92
+ ctx = Textus::Composition.context(self, role: as)
93
+ Textus::Composition.writes_put(ctx).call(
94
+ key, meta: meta, body: body, content: content, if_etag: if_etag, suppress_events: suppress_events
95
+ )
96
+ end
97
+ # rubocop:enable Metrics/ParameterLists
86
98
 
87
- def delete(...) = @writer.delete(...)
99
+ def delete(key, if_etag: nil, as: Role::DEFAULT, suppress_events: false)
100
+ ctx = Textus::Composition.context(self, role: as)
101
+ Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag, suppress_events: suppress_events)
102
+ end
88
103
 
89
104
  def fire_event(event, **)
90
- view = Store::View.new(self)
105
+ view = Textus::Application::Context.new(store: self, role: "human")
91
106
  @bus.publish(event, store: view, **)
92
107
  end
93
108
 
94
- def accept(...) = @writer.accept(...)
109
+ def accept(key, as: Role::DEFAULT)
110
+ ctx = Textus::Composition.context(self, role: as)
111
+ Textus::Composition.writes_accept(ctx).call(key)
112
+ end
113
+
114
+ def reject(...) = @writer.reject(...)
95
115
 
96
116
  def deps(key) = @reader.deps(key)
97
117
  def rdeps(key) = @reader.rdeps(key)
@@ -104,9 +124,9 @@ module Textus
104
124
  # Move an entry from old_key to new_key within the same zone. Preserves
105
125
  # uid (minting one first if absent), validates both keys against the
106
126
  # manifest, refuses to clobber, and writes one mv audit row.
107
- def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false)
108
- Mover.new(reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
109
- .call(old_key, new_key, as: as, dry_run: dry_run)
127
+ def mv(old_key, new_key, as: Role::DEFAULT, dry_run: false, correlation_id: nil)
128
+ Mover.new(store: self, reader: @reader, writer: @writer, manifest: @manifest, audit_log: audit_log)
129
+ .call(old_key, new_key, as: as, dry_run: dry_run, correlation_id: correlation_id)
110
130
  end
111
131
 
112
132
  def audit_log
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.8.1"
2
+ VERSION = "0.10.0"
3
3
  PROTOCOL = "textus/2"
4
4
  end
data/lib/textus.rb CHANGED
@@ -13,4 +13,5 @@ loader.setup
13
13
  loader.eager_load
14
14
 
15
15
  module Textus
16
+ extend Hooks::Dsl
16
17
  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.8.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -109,7 +109,20 @@ files:
109
109
  - docs/conventions.md
110
110
  - exe/textus
111
111
  - lib/textus.rb
112
- - lib/textus/builder.rb
112
+ - lib/textus/application/context.rb
113
+ - lib/textus/application/reads/audit.rb
114
+ - lib/textus/application/reads/blame.rb
115
+ - lib/textus/application/reads/freshness.rb
116
+ - lib/textus/application/reads/get.rb
117
+ - lib/textus/application/reads/policy_explain.rb
118
+ - lib/textus/application/refresh/all.rb
119
+ - lib/textus/application/refresh/orchestrator.rb
120
+ - lib/textus/application/refresh/worker.rb
121
+ - lib/textus/application/writes/accept.rb
122
+ - lib/textus/application/writes/build.rb
123
+ - lib/textus/application/writes/delete.rb
124
+ - lib/textus/application/writes/publish.rb
125
+ - lib/textus/application/writes/put.rb
113
126
  - lib/textus/builder/pipeline.rb
114
127
  - lib/textus/builder/renderer.rb
115
128
  - lib/textus/builder/renderer/json.rb
@@ -120,13 +133,17 @@ files:
120
133
  - lib/textus/cli/group.rb
121
134
  - lib/textus/cli/group/hook.rb
122
135
  - lib/textus/cli/group/key.rb
136
+ - lib/textus/cli/group/policy.rb
123
137
  - lib/textus/cli/group/schema.rb
124
138
  - lib/textus/cli/verb.rb
125
139
  - lib/textus/cli/verb/accept.rb
140
+ - lib/textus/cli/verb/audit.rb
141
+ - lib/textus/cli/verb/blame.rb
126
142
  - lib/textus/cli/verb/build.rb
127
143
  - lib/textus/cli/verb/delete.rb
128
144
  - lib/textus/cli/verb/deps.rb
129
145
  - lib/textus/cli/verb/doctor.rb
146
+ - lib/textus/cli/verb/freshness.rb
130
147
  - lib/textus/cli/verb/get.rb
131
148
  - lib/textus/cli/verb/hook_run.rb
132
149
  - lib/textus/cli/verb/hooks.rb
@@ -135,29 +152,48 @@ files:
135
152
  - lib/textus/cli/verb/list.rb
136
153
  - lib/textus/cli/verb/migrate_keys.rb
137
154
  - lib/textus/cli/verb/mv.rb
155
+ - lib/textus/cli/verb/policy_explain.rb
156
+ - lib/textus/cli/verb/policy_list.rb
138
157
  - lib/textus/cli/verb/published.rb
139
158
  - lib/textus/cli/verb/put.rb
140
159
  - lib/textus/cli/verb/rdeps.rb
141
160
  - lib/textus/cli/verb/refresh.rb
161
+ - lib/textus/cli/verb/refresh_stale.rb
162
+ - lib/textus/cli/verb/reject.rb
142
163
  - lib/textus/cli/verb/schema.rb
143
164
  - lib/textus/cli/verb/schema_diff.rb
144
165
  - lib/textus/cli/verb/schema_init.rb
145
166
  - lib/textus/cli/verb/schema_migrate.rb
146
- - lib/textus/cli/verb/stale.rb
147
167
  - lib/textus/cli/verb/uid.rb
148
168
  - lib/textus/cli/verb/where.rb
169
+ - lib/textus/composition.rb
149
170
  - lib/textus/dependencies.rb
150
171
  - lib/textus/doctor.rb
151
172
  - lib/textus/doctor/check.rb
152
173
  - lib/textus/doctor/check/audit_log.rb
174
+ - lib/textus/doctor/check/handler_allowlist.rb
153
175
  - lib/textus/doctor/check/hooks.rb
154
176
  - lib/textus/doctor/check/illegal_keys.rb
177
+ - lib/textus/doctor/check/intake_registration.rb
178
+ - lib/textus/doctor/check/legacy_intake_fields.rb
155
179
  - lib/textus/doctor/check/manifest_files.rb
180
+ - lib/textus/doctor/check/policy_ambiguity.rb
156
181
  - lib/textus/doctor/check/schema_violations.rb
157
182
  - lib/textus/doctor/check/schemas.rb
158
183
  - lib/textus/doctor/check/sentinels.rb
159
184
  - lib/textus/doctor/check/templates.rb
160
185
  - lib/textus/doctor/check/unowned_schema_fields.rb
186
+ - lib/textus/domain/action.rb
187
+ - lib/textus/domain/freshness/evaluator.rb
188
+ - lib/textus/domain/freshness/policy.rb
189
+ - lib/textus/domain/freshness/verdict.rb
190
+ - lib/textus/domain/outcome.rb
191
+ - lib/textus/domain/permission.rb
192
+ - lib/textus/domain/policy.rb
193
+ - lib/textus/domain/policy/handler_allowlist.rb
194
+ - lib/textus/domain/policy/matcher.rb
195
+ - lib/textus/domain/policy/promote.rb
196
+ - lib/textus/domain/policy/refresh.rb
161
197
  - lib/textus/entry.rb
162
198
  - lib/textus/entry/base.rb
163
199
  - lib/textus/entry/json.rb
@@ -169,8 +205,14 @@ files:
169
205
  - lib/textus/etag.rb
170
206
  - lib/textus/hooks/builtin.rb
171
207
  - lib/textus/hooks/dispatcher.rb
208
+ - lib/textus/hooks/dsl.rb
172
209
  - lib/textus/hooks/loader.rb
173
210
  - lib/textus/hooks/registry.rb
211
+ - lib/textus/infra/clock.rb
212
+ - lib/textus/infra/event_bus.rb
213
+ - lib/textus/infra/publisher.rb
214
+ - lib/textus/infra/refresh/detached.rb
215
+ - lib/textus/infra/refresh/lock.rb
174
216
  - lib/textus/init.rb
175
217
  - lib/textus/intro.rb
176
218
  - lib/textus/key/distance.rb
@@ -178,11 +220,11 @@ files:
178
220
  - lib/textus/key/path.rb
179
221
  - lib/textus/manifest.rb
180
222
  - lib/textus/manifest/entry.rb
223
+ - lib/textus/manifest/policies.rb
181
224
  - lib/textus/migrate_keys.rb
182
225
  - lib/textus/mustache.rb
183
226
  - lib/textus/projection.rb
184
227
  - lib/textus/proposal.rb
185
- - lib/textus/publisher.rb
186
228
  - lib/textus/refresh.rb
187
229
  - lib/textus/role.rb
188
230
  - lib/textus/schema.rb
@@ -193,7 +235,6 @@ files:
193
235
  - lib/textus/store/reader.rb
194
236
  - lib/textus/store/staleness.rb
195
237
  - lib/textus/store/validator.rb
196
- - lib/textus/store/view.rb
197
238
  - lib/textus/store/writer.rb
198
239
  - lib/textus/version.rb
199
240
  homepage: https://github.com/patrick204nqh/textus
@@ -1,86 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- class Builder
5
- def initialize(store)
6
- @store = store
7
- @manifest = store.manifest
8
- @root = store.root
9
- end
10
-
11
- def build(prefix: nil)
12
- built = []
13
- @manifest.entries.each do |mentry|
14
- next unless derived_zone?(mentry)
15
- next unless mentry.projection || mentry.template
16
- next if prefix && !mentry.key.start_with?(prefix)
17
-
18
- result = materialize(mentry)
19
- built << result
20
- end
21
- published_leaves = publish_leaves(prefix: prefix)
22
- { "protocol" => Textus::PROTOCOL, "built" => built, "published_leaves" => published_leaves }
23
- end
24
-
25
- private
26
-
27
- def publish_leaves(prefix: nil)
28
- repo_root = File.dirname(@root)
29
- out = []
30
- @manifest.entries.each do |mentry|
31
- next unless mentry.nested && mentry.publish_each
32
- next if prefix && !mentry.key.start_with?(prefix) && !prefix.start_with?("#{mentry.key}.")
33
-
34
- @manifest.enumerate(prefix: mentry.key).each do |row|
35
- next unless row[:manifest_entry].equal?(mentry)
36
- next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
37
-
38
- target_rel = mentry.publish_target_for(row[:key])
39
- target_abs = File.expand_path(File.join(repo_root, target_rel))
40
- unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
41
- raise PublishError.new(
42
- "entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
43
- )
44
- end
45
-
46
- Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
47
- out << { "key" => row[:key], "source" => row[:path], "target" => target_abs }
48
- end
49
- end
50
- out
51
- end
52
-
53
- def derived_zone?(mentry)
54
- writers = @manifest.zone_writers(mentry.zone)
55
- writers.include?("build")
56
- end
57
-
58
- def materialize(mentry)
59
- target_path = Pipeline.run(
60
- store: @store,
61
- mentry: mentry,
62
- template_loader: ->(name) { read_template(name) },
63
- )
64
- publish_and_fire(mentry, target_path)
65
- { "key" => mentry.key, "path" => target_path, "published_to" => mentry.publish_to }
66
- end
67
-
68
- def read_template(name)
69
- tpl_path = File.join(@root, "templates", name)
70
- raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
71
-
72
- File.read(tpl_path)
73
- end
74
-
75
- def publish_and_fire(mentry, target_path)
76
- mentry.publish_to.each do |rel|
77
- repo_root = File.dirname(@root)
78
- Publisher.publish(source: target_path, target: File.join(repo_root, rel), store_root: @root)
79
- end
80
-
81
- envelope = @store.get(mentry.key)
82
- @store.fire_event(:build, key: mentry.key, envelope: envelope,
83
- sources: Array(mentry.projection&.fetch("select", nil)).compact)
84
- end
85
- end
86
- end
@@ -1,14 +0,0 @@
1
- module Textus
2
- class CLI
3
- class Verb
4
- class Stale < Verb
5
- option :prefix, "--prefix=KEY"
6
- option :zone, "--zone=Z"
7
-
8
- def call(store)
9
- emit(store.stale(prefix: prefix, zone: zone))
10
- end
11
- end
12
- end
13
- end
14
- end