textus 0.26.0 → 0.30.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +118 -68
  3. data/CHANGELOG.md +132 -0
  4. data/README.md +61 -19
  5. data/SPEC.md +107 -46
  6. data/docs/conventions.md +4 -4
  7. data/lib/textus/boot.rb +18 -12
  8. data/lib/textus/builder/pipeline.rb +13 -12
  9. data/lib/textus/call.rb +28 -0
  10. data/lib/textus/cli/verb/audit.rb +1 -1
  11. data/lib/textus/cli/verb/boot.rb +1 -1
  12. data/lib/textus/cli/verb/build.rb +2 -2
  13. data/lib/textus/cli/verb/doctor.rb +1 -1
  14. data/lib/textus/cli/verb/hook_run.rb +2 -6
  15. data/lib/textus/cli/verb/put.rb +5 -14
  16. data/lib/textus/cli/verb/retain.rb +19 -0
  17. data/lib/textus/cli/verb/rule_list.rb +1 -1
  18. data/lib/textus/cli/verb.rb +6 -6
  19. data/lib/textus/cli.rb +19 -23
  20. data/lib/textus/container.rb +23 -0
  21. data/lib/textus/dispatcher.rb +57 -0
  22. data/lib/textus/doctor/check/audit_log.rb +1 -1
  23. data/lib/textus/doctor/check/schema_violations.rb +1 -1
  24. data/lib/textus/doctor/check/sentinels.rb +10 -8
  25. data/lib/textus/doctor/check.rb +15 -5
  26. data/lib/textus/doctor.rb +7 -7
  27. data/lib/textus/domain/authorizer.rb +2 -2
  28. data/lib/textus/domain/duration.rb +22 -0
  29. data/lib/textus/domain/policy/refresh.rb +1 -15
  30. data/lib/textus/domain/policy/retention.rb +26 -0
  31. data/lib/textus/domain/retention.rb +44 -0
  32. data/lib/textus/domain/sentinel.rb +9 -65
  33. data/lib/textus/domain/staleness/generator_check.rb +46 -26
  34. data/lib/textus/domain/staleness/intake_check.rb +18 -10
  35. data/lib/textus/domain/staleness.rb +3 -3
  36. data/lib/textus/{application/envelope → envelope/io}/reader.rb +6 -2
  37. data/lib/textus/{application/envelope → envelope/io}/writer.rb +19 -11
  38. data/lib/textus/hooks/context.rb +30 -13
  39. data/lib/textus/hooks/event_bus.rb +8 -20
  40. data/lib/textus/hooks/rpc_registry.rb +9 -35
  41. data/lib/textus/hooks/signature.rb +31 -0
  42. data/lib/textus/init.rb +7 -6
  43. data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
  44. data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
  45. data/lib/textus/maintenance/migrate.rb +51 -0
  46. data/lib/textus/maintenance/rule_lint.rb +56 -0
  47. data/lib/textus/maintenance/zone_mv.rb +51 -0
  48. data/lib/textus/maintenance.rb +15 -0
  49. data/lib/textus/manifest/data.rb +9 -4
  50. data/lib/textus/manifest/entry/base.rb +38 -18
  51. data/lib/textus/manifest/entry/derived.rb +6 -6
  52. data/lib/textus/manifest/entry/nested.rb +7 -9
  53. data/lib/textus/manifest/entry/parser.rb +2 -2
  54. data/lib/textus/manifest/entry/validators/events.rb +1 -1
  55. data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
  56. data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
  57. data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
  58. data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
  59. data/lib/textus/manifest/entry/validators.rb +2 -2
  60. data/lib/textus/manifest/entry.rb +0 -5
  61. data/lib/textus/manifest/policy.rb +34 -7
  62. data/lib/textus/manifest/rules.rb +10 -1
  63. data/lib/textus/manifest/schema.rb +54 -4
  64. data/lib/textus/manifest.rb +4 -8
  65. data/lib/textus/mcp/server.rb +2 -11
  66. data/lib/textus/mcp/session.rb +13 -20
  67. data/lib/textus/mcp/tools.rb +2 -2
  68. data/lib/textus/mcp.rb +1 -1
  69. data/lib/textus/{infra → ports}/audit_log.rb +1 -1
  70. data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
  71. data/lib/textus/{infra → ports}/build_lock.rb +1 -1
  72. data/lib/textus/{infra → ports}/clock.rb +1 -1
  73. data/lib/textus/{infra → ports}/publisher.rb +6 -6
  74. data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
  75. data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
  76. data/lib/textus/ports/sentinel_store.rb +67 -0
  77. data/lib/textus/ports/storage/file_stat.rb +19 -0
  78. data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
  79. data/lib/textus/projection.rb +91 -0
  80. data/lib/textus/read/audit.rb +111 -0
  81. data/lib/textus/read/blame.rb +81 -0
  82. data/lib/textus/read/boot.rb +18 -0
  83. data/lib/textus/read/deps.rb +24 -0
  84. data/lib/textus/read/doctor.rb +19 -0
  85. data/lib/textus/read/freshness.rb +101 -0
  86. data/lib/textus/read/get.rb +66 -0
  87. data/lib/textus/read/get_or_refresh.rb +69 -0
  88. data/lib/textus/read/list.rb +15 -0
  89. data/lib/textus/read/policy_explain.rb +42 -0
  90. data/lib/textus/read/published.rb +15 -0
  91. data/lib/textus/read/pulse.rb +89 -0
  92. data/lib/textus/read/rdeps.rb +25 -0
  93. data/lib/textus/read/retainable.rb +17 -0
  94. data/lib/textus/read/schema_envelope.rb +16 -0
  95. data/lib/textus/read/stale.rb +17 -0
  96. data/lib/textus/read/uid.rb +20 -0
  97. data/lib/textus/read/validate_all.rb +22 -0
  98. data/lib/textus/read/validator.rb +84 -0
  99. data/lib/textus/read/where.rb +16 -0
  100. data/lib/textus/role_scope.rb +50 -0
  101. data/lib/textus/schema/tools.rb +3 -3
  102. data/lib/textus/store.rb +16 -7
  103. data/lib/textus/version.rb +1 -1
  104. data/lib/textus/write/accept.rb +86 -0
  105. data/lib/textus/write/authority_gate.rb +24 -0
  106. data/lib/textus/write/delete.rb +40 -0
  107. data/lib/textus/write/intake_fetch.rb +23 -0
  108. data/lib/textus/write/materializer.rb +48 -0
  109. data/lib/textus/write/mv.rb +113 -0
  110. data/lib/textus/write/publish.rb +66 -0
  111. data/lib/textus/write/put.rb +45 -0
  112. data/lib/textus/write/refresh_all.rb +44 -0
  113. data/lib/textus/write/refresh_orchestrator.rb +102 -0
  114. data/lib/textus/write/refresh_worker.rb +124 -0
  115. data/lib/textus/write/reject.rb +54 -0
  116. data/lib/textus/write/retention_sweep.rb +55 -0
  117. data/lib/textus.rb +1 -2
  118. metadata +62 -50
  119. data/lib/textus/application/caps.rb +0 -49
  120. data/lib/textus/application/context.rb +0 -34
  121. data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
  122. data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
  123. data/lib/textus/application/maintenance/migrate.rb +0 -59
  124. data/lib/textus/application/maintenance/rule_lint.rb +0 -65
  125. data/lib/textus/application/maintenance/zone_mv.rb +0 -60
  126. data/lib/textus/application/maintenance.rb +0 -17
  127. data/lib/textus/application/projection.rb +0 -93
  128. data/lib/textus/application/read/audit.rb +0 -106
  129. data/lib/textus/application/read/blame.rb +0 -91
  130. data/lib/textus/application/read/deps.rb +0 -34
  131. data/lib/textus/application/read/freshness.rb +0 -110
  132. data/lib/textus/application/read/get.rb +0 -75
  133. data/lib/textus/application/read/get_or_refresh.rb +0 -63
  134. data/lib/textus/application/read/list.rb +0 -25
  135. data/lib/textus/application/read/policy_explain.rb +0 -47
  136. data/lib/textus/application/read/published.rb +0 -25
  137. data/lib/textus/application/read/pulse.rb +0 -101
  138. data/lib/textus/application/read/rdeps.rb +0 -35
  139. data/lib/textus/application/read/schema_envelope.rb +0 -26
  140. data/lib/textus/application/read/stale.rb +0 -23
  141. data/lib/textus/application/read/uid.rb +0 -30
  142. data/lib/textus/application/read/validate_all.rb +0 -32
  143. data/lib/textus/application/read/validator.rb +0 -86
  144. data/lib/textus/application/read/where.rb +0 -26
  145. data/lib/textus/application/use_case.rb +0 -22
  146. data/lib/textus/application/write/accept.rb +0 -102
  147. data/lib/textus/application/write/authority_gate.rb +0 -26
  148. data/lib/textus/application/write/delete.rb +0 -45
  149. data/lib/textus/application/write/materializer.rb +0 -49
  150. data/lib/textus/application/write/mv.rb +0 -118
  151. data/lib/textus/application/write/publish.rb +0 -96
  152. data/lib/textus/application/write/put.rb +0 -49
  153. data/lib/textus/application/write/refresh_all.rb +0 -63
  154. data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
  155. data/lib/textus/application/write/refresh_worker.rb +0 -134
  156. data/lib/textus/application/write/reject.rb +0 -62
  157. data/lib/textus/session.rb +0 -84
@@ -1,101 +0,0 @@
1
- require "digest"
2
- require "time"
3
-
4
- module Textus
5
- module Application
6
- module Read
7
- # Aggregator over audit + freshness + review + doctor. One round-trip
8
- # for an agent's per-turn heartbeat. All component reads are existing
9
- # APIs; pulse is sugar with a stable envelope shape and a monotonic
10
- # cursor (seq).
11
- module Pulse
12
- def self.call(*, session:, ctx:, caps:, **)
13
- Impl.new(ctx: ctx, caps: caps, session: session).call(*, **)
14
- end
15
-
16
- class Impl
17
- def initialize(ctx:, caps:, session:)
18
- @ctx = ctx
19
- @caps = caps
20
- @manifest = caps.manifest
21
- @file_store = caps.file_store
22
- @audit_log = caps.audit_log
23
- @root = caps.root
24
- @events = caps.events
25
- @session = session
26
- end
27
-
28
- def call(since: 0)
29
- freshness_rows = freshness.call
30
- {
31
- "cursor" => @audit_log.latest_seq,
32
- "changed" => audit_changes_since(since),
33
- "stale" => freshness_rows.select { |r| r[:status] == :stale }.map { |r| r[:key] },
34
- "pending_review" => review_keys,
35
- "doctor" => doctor_summary,
36
- "manifest_etag" => manifest_etag,
37
- "next_due_at" => soonest_due(freshness_rows),
38
- "hook_errors" => hook_errors_since(since),
39
- }
40
- end
41
-
42
- private
43
-
44
- def audit_changes_since(seq)
45
- Read::Audit::Impl.new(caps: @caps).call(seq_since: seq)
46
- end
47
-
48
- def freshness
49
- @freshness ||= Read::Freshness::Impl.new(ctx: @ctx, caps: @caps)
50
- end
51
-
52
- def soonest_due(rows)
53
- times = rows.map { |r| r[:next_due_at] }.compact.map { |t| Time.parse(t) }
54
- return nil if times.empty?
55
-
56
- times.min.utc.iso8601
57
- end
58
-
59
- def review_keys
60
- # List constructor takes only manifest:; returns hashes with string keys.
61
- # Guard: zones is a Hash keyed by name string.
62
- return [] unless @manifest.data.zones.key?("review")
63
-
64
- rows = Read::List::Impl.new(caps: @caps).call(zone: "review")
65
- rows.map { |r| r.is_a?(Hash) ? (r["key"] || r[:key]) : r }
66
- end
67
-
68
- def doctor_summary
69
- result = Textus::Doctor.run(@session)
70
- issues = result["issues"] || []
71
- {
72
- "ok" => result["ok"],
73
- "warn" => issues.count { |i| i["level"] == "warning" },
74
- "fail" => issues.count { |i| i["level"] == "error" },
75
- }
76
- end
77
-
78
- def manifest_etag
79
- Digest::SHA256.hexdigest(File.read(File.join(@root, "manifest.yaml")))
80
- end
81
-
82
- def hook_errors_since(seq)
83
- @events.error_log.since(seq).map do |r|
84
- {
85
- "seq" => r[:seq],
86
- "event" => r[:event].to_s,
87
- "hook" => r[:hook].to_s,
88
- "key" => r[:key],
89
- "error_class" => r[:error_class],
90
- "error_message" => r[:error_message],
91
- "at" => r[:at],
92
- }
93
- end
94
- end
95
- end
96
- end
97
- end
98
- end
99
- end
100
-
101
- Textus::Application::UseCase.register(:pulse, Textus::Application::Read::Pulse, caps: :read)
@@ -1,35 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module Rdeps
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(caps:)
11
- @manifest = caps.manifest
12
- end
13
-
14
- def call(key)
15
- @manifest.data.entries.each_with_object([]) do |e, acc|
16
- next unless e.is_a?(Textus::Manifest::Entry::Derived)
17
-
18
- src = e.source
19
- sources = if src.is_a?(Textus::Manifest::Entry::Derived::Projection)
20
- Array(src.select).compact
21
- elsif src.is_a?(Textus::Manifest::Entry::Derived::External)
22
- Array(src.sources).compact
23
- else
24
- []
25
- end
26
- acc << e.key if sources.any? { |s| s == key || key.start_with?("#{s}.") }
27
- end
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
34
-
35
- Textus::Application::UseCase.register(:rdeps, Textus::Application::Read::Rdeps, caps: :read)
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module SchemaEnvelope
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(caps:)
11
- @manifest = caps.manifest
12
- @schemas = caps.schemas
13
- end
14
-
15
- def call(key)
16
- mentry = @manifest.resolver.resolve(key).entry
17
- schema = @schemas.fetch_or_nil(mentry.schema)
18
- { "protocol" => PROTOCOL, "key" => key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end
25
-
26
- Textus::Application::UseCase.register(:schema_envelope, Textus::Application::Read::SchemaEnvelope, caps: :read)
@@ -1,23 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module Stale
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(caps:)
11
- @manifest = caps.manifest
12
- end
13
-
14
- def call(prefix: nil, zone: nil)
15
- Textus::Domain::Staleness.new(manifest: @manifest).call(prefix: prefix, zone: zone)
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
22
-
23
- Textus::Application::UseCase.register(:stale, Textus::Application::Read::Stale, caps: :read)
@@ -1,30 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module Uid
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(ctx: ctx, caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(ctx:, caps:)
11
- @ctx = ctx
12
- @caps = caps
13
- end
14
-
15
- def call(key)
16
- get.get(key).uid
17
- end
18
-
19
- private
20
-
21
- def get
22
- @get ||= Get::Impl.new(ctx: @ctx, caps: @caps)
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
29
-
30
- Textus::Application::UseCase.register(:uid, Textus::Application::Read::Uid, caps: :read)
@@ -1,32 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module ValidateAll
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(ctx: ctx, caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(ctx:, caps:)
11
- @ctx = ctx
12
- @caps = caps
13
- @manifest = caps.manifest
14
- @schemas = caps.schemas
15
- @audit_log = caps.audit_log
16
- end
17
-
18
- def call
19
- Validator.new(
20
- reader: Get::Impl.new(ctx: @ctx, caps: @caps),
21
- manifest: @manifest,
22
- audit_log: @audit_log,
23
- schema_for: ->(name) { @schemas.fetch_or_nil(name) },
24
- ).call
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
31
-
32
- Textus::Application::UseCase.register(:validate_all, Textus::Application::Read::ValidateAll, caps: :read)
@@ -1,86 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- class Validator
5
- def initialize(reader:, manifest:, audit_log:, schema_for:)
6
- @reader = reader
7
- @manifest = manifest
8
- @audit_log = audit_log
9
- @schema_for = schema_for
10
- end
11
-
12
- def call
13
- violations = []
14
- check_content_violations(violations)
15
- check_role_authority_violations(violations)
16
- { "protocol" => PROTOCOL, "ok" => violations.empty?, "violations" => violations }
17
- end
18
-
19
- private
20
-
21
- def check_content_violations(violations)
22
- @manifest.resolver.enumerate.each do |row|
23
- key = row[:key]
24
- mentry = row[:manifest_entry]
25
- env = fetch_envelope(key, violations) or next
26
- schema = mentry.schema && @schema_for.call(mentry.schema)
27
- next unless schema
28
-
29
- begin
30
- validate_schema!(schema, env, mentry.format)
31
- rescue Textus::Error => e
32
- violations << { "key" => key, "code" => e.code, "message" => e.message }
33
- end
34
- end
35
- end
36
-
37
- def check_role_authority_violations(violations)
38
- @manifest.resolver.enumerate.each do |row|
39
- mentry = row[:manifest_entry]
40
- next unless mentry.schema
41
-
42
- schema = @schema_for.call(mentry.schema)
43
- next unless schema
44
-
45
- env = begin
46
- @reader.get(row[:key])
47
- rescue StandardError
48
- next
49
- end
50
- append_authority_violations(violations, row[:key], env, schema)
51
- end
52
- end
53
-
54
- def append_authority_violations(violations, key, env, schema)
55
- last_writer = @audit_log.last_writer_for(key)
56
- return if last_writer.nil?
57
-
58
- last_writer_is_authority = @manifest.policy.role_kind(last_writer) == :accept_authority
59
-
60
- env.meta.each_key do |field|
61
- owner = schema.maintained_by(field)
62
- next if owner.nil? || last_writer == owner || last_writer_is_authority
63
-
64
- violations << { "key" => key, "code" => "role_authority",
65
- "field" => field, "expected" => owner, "last_writer" => last_writer }
66
- end
67
- end
68
-
69
- def fetch_envelope(key, violations)
70
- @reader.get(key)
71
- rescue Textus::Error => e
72
- violations << { "key" => key, "code" => e.code, "message" => e.message }
73
- nil
74
- end
75
-
76
- def validate_schema!(schema, envelope, format)
77
- payload = case format
78
- when "json", "yaml" then envelope.content || {}
79
- else envelope.meta || {}
80
- end
81
- schema.validate!(payload)
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Application
3
- module Read
4
- module Where
5
- def self.call(*, session:, ctx:, caps:, **) # rubocop:disable Lint/UnusedMethodArgument
6
- Impl.new(caps: caps).call(*, **)
7
- end
8
-
9
- class Impl
10
- def initialize(caps:)
11
- @manifest = caps.manifest
12
- end
13
-
14
- def call(key)
15
- res = @manifest.resolver.resolve(key)
16
- mentry = res.entry
17
- path = res.path
18
- { "protocol" => PROTOCOL, "key" => key, "zone" => mentry.zone, "owner" => mentry.owner, "path" => path }
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end
25
-
26
- Textus::Application::UseCase.register(:where, Textus::Application::Read::Where, caps: :read)
@@ -1,22 +0,0 @@
1
- module Textus
2
- module Application
3
- # Registry mapping verb symbols to use-case modules. Each entry says
4
- # which caps slice the use case needs (:read or :write); Session
5
- # uses this to define one method per verb.
6
- module UseCase
7
- Entry = Data.define(:verb, :mod, :caps_kind)
8
-
9
- @entries = []
10
-
11
- class << self
12
- attr_reader :entries
13
-
14
- def register(verb, mod, caps:)
15
- @entries << Entry.new(verb: verb.to_sym, mod: mod, caps_kind: caps.to_sym)
16
- end
17
-
18
- def each(&) = @entries.each(&)
19
- end
20
- end
21
- end
22
- end
@@ -1,102 +0,0 @@
1
- require_relative "authority_gate"
2
-
3
- module Textus
4
- module Application
5
- module Write
6
- module Accept
7
- def self.call(*, session:, ctx:, caps:, **)
8
- Impl.new(
9
- ctx: ctx, caps: caps,
10
- writer: session.envelope_writer,
11
- hook_context: session.hook_context
12
- ).call(*, **)
13
- end
14
-
15
- class Impl
16
- include AuthorityGate
17
-
18
- def initialize(ctx:, caps:, writer:, hook_context:)
19
- @ctx = ctx
20
- @caps = caps
21
- @manifest = caps.manifest
22
- @file_store = caps.file_store
23
- @schemas = caps.schemas
24
- @writer = writer
25
- @events = caps.events
26
- @authorizer = caps.authorizer
27
- @hook_context = hook_context
28
- end
29
-
30
- def call(pending_key)
31
- assert_accept_authority!("accept")
32
-
33
- env = Textus::Application::Read::Get::Impl.new(
34
- ctx: @ctx, caps: @caps,
35
- ).call(pending_key)
36
- proposal = env.meta["proposal"] or raise ProposalError.new("entry has no proposal block: #{pending_key}")
37
- target = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
38
- action = proposal["action"] || "put"
39
-
40
- evaluate_promotion!(env, target)
41
-
42
- case action
43
- when "put"
44
- # Nested proposal "frontmatter" — the meta to write to the accepted
45
- # target. Not related to the removed intake-handler legacy bridge.
46
- target_meta = env.meta["frontmatter"] || {}
47
- target_body = env.body
48
- put_op.call(target, meta: target_meta, body: target_body)
49
- when "delete"
50
- delete_op.call(target)
51
- else
52
- raise ProposalError.new("unknown action: #{action}")
53
- end
54
-
55
- delete_op.call(pending_key)
56
-
57
- @events.publish(:proposal_accepted,
58
- ctx: @hook_context,
59
- key: pending_key,
60
- target_key: target)
61
-
62
- { "protocol" => PROTOCOL, "accepted" => pending_key, "target_key" => target, "action" => action }
63
- end
64
-
65
- private
66
-
67
- def put_op
68
- @put_op ||= Textus::Application::Write::Put::Impl.new(
69
- ctx: @ctx, caps: @caps, writer: @writer,
70
- hook_context: @hook_context
71
- )
72
- end
73
-
74
- def delete_op
75
- @delete_op ||= Textus::Application::Write::Delete::Impl.new(
76
- ctx: @ctx, caps: @caps, writer: @writer,
77
- hook_context: @hook_context
78
- )
79
- end
80
-
81
- def evaluate_promotion!(env, target_key)
82
- rules = @manifest.rules.for(target_key)
83
- promote = rules.promote
84
- return if promote.nil? || promote.requires.empty?
85
-
86
- policy = Textus::Domain::Policy::Promotion.from_names(promote.requires)
87
- result = policy.evaluate(
88
- entry: env, schemas: @schemas, manifest: @manifest, role: @ctx.role,
89
- )
90
- return if result.ok?
91
-
92
- raise ProposalError.new(
93
- "promotion gate failed: #{result.reasons.join("; ")}",
94
- )
95
- end
96
- end
97
- end
98
- end
99
- end
100
- end
101
-
102
- Textus::Application::UseCase.register(:accept, Textus::Application::Write::Accept, caps: :write)
@@ -1,26 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- # Shared gate for write verbs that require the caller to hold the
5
- # manifest's accept_authority role. Provides one method, expressed
6
- # as two early-returns rather than a ternary, so each failure mode
7
- # reads on its own line.
8
- module AuthorityGate
9
- def assert_accept_authority!(verb)
10
- return if @manifest.policy.role_kind(@ctx.role) == :accept_authority
11
-
12
- authority = @manifest.policy.roles_with_kind(:accept_authority).first
13
- if authority.nil?
14
- raise ProposalError.new(
15
- "no role with accept_authority kind is declared in this manifest; #{verb} is disabled",
16
- )
17
- end
18
-
19
- raise ProposalError.new(
20
- "only #{authority} role can #{verb} proposals; got '#{@ctx.role}'",
21
- )
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,45 +0,0 @@
1
- module Textus
2
- module Application
3
- module Write
4
- module Delete
5
- def self.call(*, session:, ctx:, caps:, **)
6
- Impl.new(
7
- ctx: ctx, caps: caps,
8
- writer: session.envelope_writer,
9
- hook_context: session.hook_context
10
- ).call(*, **)
11
- end
12
-
13
- class Impl
14
- def initialize(ctx:, caps:, writer:, hook_context:)
15
- @ctx = ctx
16
- @manifest = caps.manifest
17
- @events = caps.events
18
- @authorizer = caps.authorizer
19
- @writer = writer
20
- @hook_context = hook_context
21
- end
22
-
23
- def call(key, if_etag: nil, suppress_events: false)
24
- Textus::Manifest::Data.validate_key!(key)
25
- mentry = @manifest.resolver.resolve(key).entry
26
-
27
- @authorizer.authorize_write!(mentry, role: @ctx.role)
28
-
29
- @writer.delete(key, mentry: mentry, if_etag: if_etag)
30
-
31
- unless suppress_events
32
- @events.publish(:entry_deleted,
33
- ctx: @hook_context,
34
- key: key)
35
- end
36
-
37
- { "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
44
-
45
- Textus::Application::UseCase.register(:delete, Textus::Application::Write::Delete, caps: :write)
@@ -1,49 +0,0 @@
1
- require "fileutils"
2
-
3
- module Textus
4
- module Application
5
- module Write
6
- # Materializes a single Derived manifest entry onto disk by running
7
- # the builder pipeline (template + projection + external runner).
8
- # Extracted from Application::Write::Build so that Publish can reuse
9
- # it without creating a Build dependency.
10
- class Materializer
11
- def initialize(ctx:, caps:, rpc:, session:)
12
- @ctx = ctx
13
- @caps = caps
14
- @manifest = caps.manifest
15
- @file_store = caps.file_store
16
- @rpc = rpc
17
- @root = caps.root
18
- @session = session
19
- end
20
-
21
- # Runs the builder pipeline for `mentry` and returns the on-disk
22
- # target_path string.
23
- def run(mentry)
24
- reader = Textus::Application::Read::Get::Impl.new(ctx: @ctx, caps: @caps)
25
- lister = Textus::Application::Read::List::Impl.new(caps: @caps)
26
- Builder::Pipeline.run(
27
- mentry: mentry,
28
- manifest: @manifest,
29
- reader: reader.method(:call),
30
- lister: lister.method(:call),
31
- rpc: @rpc,
32
- template_loader: ->(name) { read_template(name) },
33
- transform_context: @caps,
34
- inject_boot: -> { Textus::Boot.run(@session) },
35
- )
36
- end
37
-
38
- private
39
-
40
- def read_template(name)
41
- tpl_path = File.join(@root, "templates", name)
42
- raise TemplateError.new("template not found: #{tpl_path}", template_name: name) unless File.exist?(tpl_path)
43
-
44
- File.read(tpl_path)
45
- end
46
- end
47
- end
48
- end
49
- end