textus 0.26.0 → 0.29.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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +111 -67
- data/CHANGELOG.md +76 -0
- data/README.md +55 -13
- data/SPEC.md +75 -38
- data/docs/conventions.md +4 -4
- data/lib/textus/boot.rb +14 -10
- data/lib/textus/builder/pipeline.rb +13 -12
- data/lib/textus/call.rb +28 -0
- data/lib/textus/cli/verb/audit.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +2 -2
- data/lib/textus/cli/verb/put.rb +3 -3
- data/lib/textus/cli/verb.rb +6 -6
- data/lib/textus/cli.rb +0 -7
- data/lib/textus/container.rb +23 -0
- data/lib/textus/dispatcher.rb +49 -0
- data/lib/textus/doctor/check/audit_log.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/sentinels.rb +10 -8
- data/lib/textus/doctor/check.rb +12 -5
- data/lib/textus/doctor.rb +7 -7
- data/lib/textus/domain/authorizer.rb +2 -2
- data/lib/textus/domain/sentinel.rb +9 -65
- data/lib/textus/domain/staleness/generator_check.rb +46 -26
- data/lib/textus/domain/staleness/intake_check.rb +18 -10
- data/lib/textus/domain/staleness.rb +3 -3
- data/lib/textus/{application/envelope → envelope/io}/reader.rb +2 -2
- data/lib/textus/{application/envelope → envelope/io}/writer.rb +11 -11
- data/lib/textus/hooks/context.rb +30 -13
- data/lib/textus/hooks/rpc_registry.rb +1 -1
- data/lib/textus/maintenance/key_delete_prefix.rb +36 -0
- data/lib/textus/maintenance/key_mv_prefix.rb +46 -0
- data/lib/textus/maintenance/migrate.rb +51 -0
- data/lib/textus/maintenance/rule_lint.rb +56 -0
- data/lib/textus/maintenance/zone_mv.rb +51 -0
- data/lib/textus/maintenance.rb +15 -0
- data/lib/textus/manifest/data.rb +4 -3
- data/lib/textus/manifest/entry/base.rb +38 -18
- data/lib/textus/manifest/entry/derived.rb +6 -6
- data/lib/textus/manifest/entry/nested.rb +7 -9
- data/lib/textus/manifest/entry/parser.rb +2 -2
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/entry/validators/format_matrix.rb +2 -2
- data/lib/textus/manifest/entry/validators/index_filename.rb +1 -1
- data/lib/textus/manifest/entry/validators/inject_boot.rb +4 -2
- data/lib/textus/manifest/entry/validators/publish_each.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +2 -2
- data/lib/textus/manifest/entry.rb +0 -5
- data/lib/textus/manifest.rb +1 -6
- data/lib/textus/mcp/server.rb +1 -2
- data/lib/textus/mcp/session.rb +10 -1
- data/lib/textus/mcp/tools.rb +2 -2
- data/lib/textus/mcp.rb +1 -1
- data/lib/textus/{infra → ports}/audit_log.rb +1 -1
- data/lib/textus/{infra → ports}/audit_subscriber.rb +2 -2
- data/lib/textus/{infra → ports}/build_lock.rb +1 -1
- data/lib/textus/{infra → ports}/clock.rb +1 -1
- data/lib/textus/{infra → ports}/publisher.rb +6 -6
- data/lib/textus/{infra → ports}/refresh/detached.rb +3 -3
- data/lib/textus/{infra → ports}/refresh/lock.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +67 -0
- data/lib/textus/ports/storage/file_stat.rb +19 -0
- data/lib/textus/{infra → ports}/storage/file_store.rb +1 -1
- data/lib/textus/projection.rb +91 -0
- data/lib/textus/read/audit.rb +111 -0
- data/lib/textus/read/blame.rb +81 -0
- data/lib/textus/read/boot.rb +18 -0
- data/lib/textus/read/deps.rb +24 -0
- data/lib/textus/read/doctor.rb +19 -0
- data/lib/textus/read/freshness.rb +101 -0
- data/lib/textus/read/get.rb +66 -0
- data/lib/textus/read/get_or_refresh.rb +69 -0
- data/lib/textus/read/list.rb +15 -0
- data/lib/textus/read/policy_explain.rb +37 -0
- data/lib/textus/read/published.rb +15 -0
- data/lib/textus/read/pulse.rb +89 -0
- data/lib/textus/read/rdeps.rb +25 -0
- data/lib/textus/read/schema_envelope.rb +16 -0
- data/lib/textus/read/stale.rb +17 -0
- data/lib/textus/read/uid.rb +20 -0
- data/lib/textus/read/validate_all.rb +22 -0
- data/lib/textus/read/validator.rb +84 -0
- data/lib/textus/read/where.rb +16 -0
- data/lib/textus/role_scope.rb +49 -0
- data/lib/textus/schema/tools.rb +3 -3
- data/lib/textus/store.rb +16 -7
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +86 -0
- data/lib/textus/write/authority_gate.rb +24 -0
- data/lib/textus/write/delete.rb +54 -0
- data/lib/textus/write/materializer.rb +48 -0
- data/lib/textus/write/mv.rb +123 -0
- data/lib/textus/write/publish.rb +66 -0
- data/lib/textus/write/put.rb +59 -0
- data/lib/textus/write/refresh_all.rb +44 -0
- data/lib/textus/write/refresh_orchestrator.rb +102 -0
- data/lib/textus/write/refresh_worker.rb +138 -0
- data/lib/textus/write/reject.rb +54 -0
- data/lib/textus.rb +1 -2
- metadata +54 -50
- data/lib/textus/application/caps.rb +0 -49
- data/lib/textus/application/context.rb +0 -34
- data/lib/textus/application/maintenance/key_delete_prefix.rb +0 -44
- data/lib/textus/application/maintenance/key_mv_prefix.rb +0 -57
- data/lib/textus/application/maintenance/migrate.rb +0 -59
- data/lib/textus/application/maintenance/rule_lint.rb +0 -65
- data/lib/textus/application/maintenance/zone_mv.rb +0 -60
- data/lib/textus/application/maintenance.rb +0 -17
- data/lib/textus/application/projection.rb +0 -93
- data/lib/textus/application/read/audit.rb +0 -106
- data/lib/textus/application/read/blame.rb +0 -91
- data/lib/textus/application/read/deps.rb +0 -34
- data/lib/textus/application/read/freshness.rb +0 -110
- data/lib/textus/application/read/get.rb +0 -75
- data/lib/textus/application/read/get_or_refresh.rb +0 -63
- data/lib/textus/application/read/list.rb +0 -25
- data/lib/textus/application/read/policy_explain.rb +0 -47
- data/lib/textus/application/read/published.rb +0 -25
- data/lib/textus/application/read/pulse.rb +0 -101
- data/lib/textus/application/read/rdeps.rb +0 -35
- data/lib/textus/application/read/schema_envelope.rb +0 -26
- data/lib/textus/application/read/stale.rb +0 -23
- data/lib/textus/application/read/uid.rb +0 -30
- data/lib/textus/application/read/validate_all.rb +0 -32
- data/lib/textus/application/read/validator.rb +0 -86
- data/lib/textus/application/read/where.rb +0 -26
- data/lib/textus/application/use_case.rb +0 -22
- data/lib/textus/application/write/accept.rb +0 -102
- data/lib/textus/application/write/authority_gate.rb +0 -26
- data/lib/textus/application/write/delete.rb +0 -45
- data/lib/textus/application/write/materializer.rb +0 -49
- data/lib/textus/application/write/mv.rb +0 -118
- data/lib/textus/application/write/publish.rb +0 -96
- data/lib/textus/application/write/put.rb +0 -49
- data/lib/textus/application/write/refresh_all.rb +0 -63
- data/lib/textus/application/write/refresh_orchestrator.rb +0 -102
- data/lib/textus/application/write/refresh_worker.rb +0 -134
- data/lib/textus/application/write/reject.rb +0 -62
- data/lib/textus/session.rb +0 -84
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Maintenance
|
|
5
|
+
# Compare the live manifest's `rules:` block against a candidate
|
|
6
|
+
# YAML string. Returns a Plan describing rule additions/removals/
|
|
7
|
+
# changes. Does NOT write anything.
|
|
8
|
+
class RuleLint
|
|
9
|
+
def initialize(container:, call:)
|
|
10
|
+
@container = container
|
|
11
|
+
@call = call
|
|
12
|
+
@root = container.root
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(candidate_yaml:)
|
|
16
|
+
live_rules = current_rules
|
|
17
|
+
candidate_rules = parse_candidate(candidate_yaml)
|
|
18
|
+
|
|
19
|
+
live_by_match = live_rules.to_h { |r| [r["match"], r] }
|
|
20
|
+
candidate_by_match = candidate_rules.to_h { |r| [r["match"], r] }
|
|
21
|
+
|
|
22
|
+
steps = (candidate_by_match.keys - live_by_match.keys).map do |m|
|
|
23
|
+
{ "op" => "add_rule", "match" => m, "rule" => candidate_by_match[m] }
|
|
24
|
+
end
|
|
25
|
+
(live_by_match.keys - candidate_by_match.keys).each do |m|
|
|
26
|
+
steps << { "op" => "remove_rule", "match" => m }
|
|
27
|
+
end
|
|
28
|
+
(live_by_match.keys & candidate_by_match.keys).each do |m|
|
|
29
|
+
next if live_by_match[m] == candidate_by_match[m]
|
|
30
|
+
|
|
31
|
+
steps << { "op" => "change_rule", "match" => m,
|
|
32
|
+
"from" => live_by_match[m], "to" => candidate_by_match[m] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Plan.new(steps: steps, warnings: [])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def current_rules
|
|
41
|
+
raw = YAML.safe_load_file(File.join(@root, "manifest.yaml"),
|
|
42
|
+
permitted_classes: [Symbol], aliases: false)
|
|
43
|
+
Array(raw["rules"])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse_candidate(yaml_text)
|
|
47
|
+
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
48
|
+
raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
Array(raw["rules"])
|
|
51
|
+
rescue Psych::Exception => e
|
|
52
|
+
raise UsageError.new("candidate YAML parse error: #{e.message}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Maintenance
|
|
5
|
+
# Rename a zone — rewrites the manifest's zones[] entry, rewrites
|
|
6
|
+
# the `zone:` field on every entry under the old zone, and moves
|
|
7
|
+
# every file from zones/<old>/ to zones/<new>/.
|
|
8
|
+
class ZoneMv
|
|
9
|
+
def initialize(container:, call:)
|
|
10
|
+
@container = container
|
|
11
|
+
@call = call
|
|
12
|
+
@manifest = container.manifest
|
|
13
|
+
@root = container.root
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(from:, to:, dry_run: false)
|
|
17
|
+
raise UsageError.new("from and to required") if from.nil? || to.nil? || from.empty? || to.empty?
|
|
18
|
+
raise UsageError.new("zone '#{from}' not declared") unless @manifest.data.zones.key?(from)
|
|
19
|
+
|
|
20
|
+
dest_dir = File.join(@root, "zones", to)
|
|
21
|
+
raise UsageError.new("destination 'zones/#{to}' already exists") if File.exist?(dest_dir)
|
|
22
|
+
|
|
23
|
+
affected_keys = @manifest.data.entries.select { |e| e.zone == from }.map(&:key)
|
|
24
|
+
|
|
25
|
+
steps = [{ "op" => "rename_zone", "from" => from, "to" => to }]
|
|
26
|
+
steps += affected_keys.map { |k| { "op" => "mv", "from" => k, "to" => "#{to}#{k[from.length..]}" } }
|
|
27
|
+
|
|
28
|
+
plan = Plan.new(steps: steps, warnings: [])
|
|
29
|
+
return plan if dry_run
|
|
30
|
+
|
|
31
|
+
rewrite_manifest!(from, to)
|
|
32
|
+
FileUtils.mv(File.join(@root, "zones", from), dest_dir)
|
|
33
|
+
plan
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def rewrite_manifest!(from, to)
|
|
39
|
+
path = File.join(@root, "manifest.yaml")
|
|
40
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: false)
|
|
41
|
+
raw["zones"].each { |z| z["name"] = to if z["name"] == from }
|
|
42
|
+
raw["entries"].each do |e|
|
|
43
|
+
e["zone"] = to if e["zone"] == from
|
|
44
|
+
e["key"] = e["key"].sub(/\A#{Regexp.escape(from)}(\.|\z)/, "#{to}\\1")
|
|
45
|
+
e["path"] = e["path"].sub(%r{\A#{Regexp.escape(from)}(/|\z)}, "#{to}\\1")
|
|
46
|
+
end
|
|
47
|
+
File.write(path, YAML.dump(raw))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
# Bulk and structural changes to a textus store. Each use case returns
|
|
3
|
+
# a Plan when called with dry_run: true, and applies the plan when
|
|
4
|
+
# called with dry_run: false.
|
|
5
|
+
module Maintenance
|
|
6
|
+
# A Plan is a JSON-shaped preview. Steps are op-tagged hashes the
|
|
7
|
+
# use case knows how to apply. Warnings are strings surfaced to
|
|
8
|
+
# the operator (skipped keys, ambiguities).
|
|
9
|
+
Plan = Data.define(:steps, :warnings) do
|
|
10
|
+
def to_h
|
|
11
|
+
{ "steps" => steps, "warnings" => warnings }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/textus/manifest/data.rb
CHANGED
|
@@ -41,7 +41,8 @@ module Textus
|
|
|
41
41
|
@audit_config = build_audit_config(raw)
|
|
42
42
|
@role_mapping = RoleKinds.resolve(raw["roles"])
|
|
43
43
|
# Policy is constructed before entries because Entry validators
|
|
44
|
-
# call `entry.in_generator_zone
|
|
44
|
+
# call `entry.in_generator_zone?(policy)` and similar helpers
|
|
45
|
+
# that take Policy as an argument.
|
|
45
46
|
@policy = Policy.new(self)
|
|
46
47
|
@entries = build_entries(raw)
|
|
47
48
|
validate_declared_keys!
|
|
@@ -60,8 +61,8 @@ module Textus
|
|
|
60
61
|
|
|
61
62
|
def build_entries(raw)
|
|
62
63
|
Array(raw["entries"]).map do |e|
|
|
63
|
-
entry = Manifest::Entry::Parser.call(
|
|
64
|
-
Manifest::Entry::Validators.run_all(entry)
|
|
64
|
+
entry = Manifest::Entry::Parser.call(e)
|
|
65
|
+
Manifest::Entry::Validators.run_all(entry, policy: @policy)
|
|
65
66
|
entry
|
|
66
67
|
end.freeze
|
|
67
68
|
end
|
|
@@ -2,11 +2,10 @@ module Textus
|
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
4
|
class Base < Entry
|
|
5
|
-
attr_reader :raw, :key, :path, :zone, :schema, :owner, :format, :
|
|
5
|
+
attr_reader :raw, :key, :path, :zone, :schema, :owner, :format, :publish_to
|
|
6
6
|
|
|
7
7
|
# rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
|
|
8
|
-
def initialize(
|
|
9
|
-
@manifest = manifest
|
|
8
|
+
def initialize(raw:, key:, path:, zone:, schema:, owner:, format:, publish_to: [])
|
|
10
9
|
@raw = raw
|
|
11
10
|
@key = key
|
|
12
11
|
@path = path
|
|
@@ -18,14 +17,14 @@ module Textus
|
|
|
18
17
|
end
|
|
19
18
|
# rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
|
|
20
19
|
|
|
21
|
-
def zone_writers
|
|
22
|
-
|
|
20
|
+
def zone_writers(policy)
|
|
21
|
+
policy.zone_writers(@zone)
|
|
23
22
|
rescue UsageError => e
|
|
24
23
|
raise UsageError.new("entry '#{@key}': #{e.message}")
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
def in_generator_zone? =
|
|
28
|
-
def in_proposal_zone? =
|
|
26
|
+
def in_generator_zone?(policy) = policy.zone_kinds(@zone).include?(:generator)
|
|
27
|
+
def in_proposal_zone?(policy) = policy.zone_kinds(@zone).include?(:proposer)
|
|
29
28
|
|
|
30
29
|
def nested? = false
|
|
31
30
|
def derived? = false
|
|
@@ -41,11 +40,32 @@ module Textus
|
|
|
41
40
|
def publish_each = nil
|
|
42
41
|
def index_filename = nil
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
43
|
+
# Minimal context object passed into entry `publish_via` hooks.
|
|
44
|
+
# Everything beyond the three primitives is derived. Data.define
|
|
45
|
+
# instances are frozen, so we recompute per-call rather than
|
|
46
|
+
# memoizing — RoleScope/Hooks::Context construction is cheap.
|
|
47
|
+
PublishContext = ::Data.define(:container, :call, :reader) do
|
|
48
|
+
def manifest = container.manifest
|
|
49
|
+
def root = container.root
|
|
50
|
+
def repo_root = File.dirname(container.root)
|
|
51
|
+
def events = container.events
|
|
52
|
+
|
|
53
|
+
def hook_context
|
|
54
|
+
Textus::Hooks::Context.new(scope: scope_for_hooks)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def emit(event, **payload)
|
|
58
|
+
events.publish(event, ctx: hook_context, **payload)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def scope_for_hooks
|
|
64
|
+
Textus::RoleScope.new(
|
|
65
|
+
container: container, role: call.role, dry_run: call.dry_run,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
49
69
|
|
|
50
70
|
# Subclasses override to customize publish behavior.
|
|
51
71
|
# Default: copy the stored file to each publish_to target.
|
|
@@ -59,12 +79,12 @@ module Textus
|
|
|
59
79
|
|
|
60
80
|
publish_to.each do |rel|
|
|
61
81
|
target_abs = File.join(pctx.repo_root, rel)
|
|
62
|
-
Textus::
|
|
63
|
-
pctx.emit
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
Textus::Ports::Publisher.publish(source: source_path, target: target_abs, store_root: pctx.root)
|
|
83
|
+
pctx.emit(:file_published,
|
|
84
|
+
key: @key,
|
|
85
|
+
envelope: envelope,
|
|
86
|
+
source: source_path,
|
|
87
|
+
target: target_abs)
|
|
68
88
|
end
|
|
69
89
|
|
|
70
90
|
{ kind: :built, value: { "key" => @key, "path" => source_path, "published_to" => publish_to } }
|
|
@@ -20,22 +20,22 @@ module Textus
|
|
|
20
20
|
def external? = @source.is_a?(External)
|
|
21
21
|
|
|
22
22
|
def publish_via(pctx, prefix: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
23
|
-
return nil unless in_generator_zone?
|
|
23
|
+
return nil unless in_generator_zone?(pctx.manifest.policy)
|
|
24
24
|
|
|
25
|
-
target_path = Textus::
|
|
26
|
-
|
|
25
|
+
target_path = Textus::Write::Materializer.new(
|
|
26
|
+
container: pctx.container, call: pctx.call,
|
|
27
27
|
).run(self)
|
|
28
28
|
|
|
29
29
|
envelope = pctx.reader.call(@key)
|
|
30
30
|
Array(publish_to).each do |rel|
|
|
31
31
|
target_abs = File.join(pctx.repo_root, rel)
|
|
32
|
-
Textus::
|
|
33
|
-
pctx.emit
|
|
32
|
+
Textus::Ports::Publisher.publish(source: target_path, target: target_abs, store_root: pctx.root)
|
|
33
|
+
pctx.emit(:file_published, key: @key, envelope: envelope, source: target_path, target: target_abs)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
src = @source
|
|
37
37
|
selects = src.is_a?(Projection) ? Array(src.select).compact : []
|
|
38
|
-
pctx.emit
|
|
38
|
+
pctx.emit(:build_completed, key: @key, envelope: envelope, sources: selects)
|
|
39
39
|
|
|
40
40
|
{ kind: :built, value: { "key" => @key, "path" => target_path, "published_to" => publish_to } }
|
|
41
41
|
end
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require_relative "validators/publish_each"
|
|
2
|
-
|
|
3
1
|
module Textus
|
|
4
2
|
class Manifest
|
|
5
3
|
class Entry
|
|
@@ -37,7 +35,7 @@ module Textus
|
|
|
37
35
|
return nil if @publish_each.nil?
|
|
38
36
|
|
|
39
37
|
leaves = []
|
|
40
|
-
|
|
38
|
+
pctx.manifest.resolver.enumerate(prefix: @key).each do |row|
|
|
41
39
|
next unless row[:manifest_entry].equal?(self)
|
|
42
40
|
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
43
41
|
|
|
@@ -49,12 +47,12 @@ module Textus
|
|
|
49
47
|
)
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
Textus::
|
|
53
|
-
pctx.emit
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
Textus::Ports::Publisher.publish(source: row[:path], target: target_abs, store_root: pctx.root)
|
|
51
|
+
pctx.emit(:file_published,
|
|
52
|
+
key: row[:key],
|
|
53
|
+
envelope: pctx.reader.call(row[:key]),
|
|
54
|
+
source: row[:path],
|
|
55
|
+
target: target_abs)
|
|
58
56
|
leaves << { "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
59
57
|
end
|
|
60
58
|
|
|
@@ -4,7 +4,7 @@ module Textus
|
|
|
4
4
|
module Parser
|
|
5
5
|
COMPUTE_KINDS = %w[projection external].freeze
|
|
6
6
|
|
|
7
|
-
def self.call(
|
|
7
|
+
def self.call(raw)
|
|
8
8
|
key = raw["key"] or raise UsageError.new("manifest entry missing key")
|
|
9
9
|
path = raw["path"] or raise UsageError.new("manifest entry '#{key}' missing path")
|
|
10
10
|
zone = raw["zone"] or raise UsageError.new("manifest entry '#{key}' missing zone")
|
|
@@ -14,7 +14,7 @@ module Textus
|
|
|
14
14
|
format = resolve_format(raw, path)
|
|
15
15
|
|
|
16
16
|
common = {
|
|
17
|
-
|
|
17
|
+
raw: raw,
|
|
18
18
|
key: key, path: path, zone: zone,
|
|
19
19
|
schema: raw["schema"], owner: raw["owner"],
|
|
20
20
|
format: format,
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Entry
|
|
4
4
|
module Validators
|
|
5
5
|
module Events
|
|
6
|
-
def self.call(entry)
|
|
6
|
+
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
7
|
pubsub_events = Textus::Hooks::EventBus::EVENTS.keys
|
|
8
8
|
events = entry.events
|
|
9
9
|
events.each_key do |evt|
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Entry
|
|
4
4
|
module Validators
|
|
5
5
|
module FormatMatrix
|
|
6
|
-
def self.call(entry)
|
|
6
|
+
def self.call(entry, policy:)
|
|
7
7
|
begin
|
|
8
8
|
Textus::Entry.for_format(entry.format).validate_path_extension(entry.path, entry.nested?)
|
|
9
9
|
rescue UsageError => e
|
|
@@ -17,7 +17,7 @@ module Textus
|
|
|
17
17
|
has_template = !entry.template.nil?
|
|
18
18
|
is_external = entry.derived? && entry.external?
|
|
19
19
|
is_intake = entry.intake?
|
|
20
|
-
return unless entry.in_generator_zone? && !has_template && !is_external && !is_intake &&
|
|
20
|
+
return unless entry.in_generator_zone?(policy) && !has_template && !is_external && !is_intake &&
|
|
21
21
|
%w[markdown text].include?(entry.format) && !entry.nested?
|
|
22
22
|
|
|
23
23
|
raise UsageError.new("entry '#{entry.key}': #{entry.format} entries in a generator zone require a template")
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Entry
|
|
4
4
|
module Validators
|
|
5
5
|
module IndexFilename
|
|
6
|
-
def self.call(entry)
|
|
6
|
+
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
7
7
|
# Use raw to detect misuse on non-nested entries (typed attr stubs nil on Base).
|
|
8
8
|
index_filename = entry.nested? ? entry.index_filename : entry.raw["index_filename"]
|
|
9
9
|
return if index_filename.nil?
|
|
@@ -3,10 +3,12 @@ module Textus
|
|
|
3
3
|
class Entry
|
|
4
4
|
module Validators
|
|
5
5
|
module InjectBoot
|
|
6
|
-
def self.call(entry)
|
|
6
|
+
def self.call(entry, policy:)
|
|
7
7
|
return unless entry.inject_boot
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
unless entry.in_generator_zone?(policy)
|
|
10
|
+
raise UsageError.new("entry '#{entry.key}': inject_boot: is only valid on derived entries")
|
|
11
|
+
end
|
|
10
12
|
|
|
11
13
|
return unless entry.template.nil?
|
|
12
14
|
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
VAR_RE = /\{([a-z]+)\}/
|
|
8
8
|
REQUIRED_DISCRIMINATOR_VARS = %w[leaf basename key].freeze
|
|
9
9
|
|
|
10
|
-
def self.call(entry)
|
|
10
|
+
def self.call(entry, policy: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
11
11
|
# Use raw to detect misuse on non-nested entries (typed attr stubs nil on Base).
|
|
12
12
|
publish_each = entry.nested? ? entry.publish_each : entry.raw["publish_each"]
|
|
13
13
|
return if publish_each.nil?
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
class Manifest
|
|
3
3
|
class Entry
|
|
4
|
-
# Re-exported for backward compatibility with callers that referenced these
|
|
5
|
-
# constants on Entry. Canonical source is the PublishEach validator.
|
|
6
|
-
PUBLISH_EACH_VARS = Validators::PublishEach::KNOWN_VARS
|
|
7
|
-
PUBLISH_EACH_VAR_RE = Validators::PublishEach::VAR_RE
|
|
8
|
-
|
|
9
4
|
# Populated by each Entry::* subclass at load time.
|
|
10
5
|
REGISTRY = {}
|
|
11
6
|
end
|
data/lib/textus/manifest.rb
CHANGED
|
@@ -43,17 +43,12 @@ module Textus # rubocop:disable Style/OneClassPerFile
|
|
|
43
43
|
|
|
44
44
|
def build(raw, root)
|
|
45
45
|
data = Manifest::Data.parse(raw, root: root)
|
|
46
|
-
|
|
46
|
+
new(
|
|
47
47
|
data: data,
|
|
48
48
|
resolver: Manifest::Resolver.new(data),
|
|
49
49
|
policy: data.policy,
|
|
50
50
|
rules: Manifest::Rules.parse(raw["rules"] || []),
|
|
51
51
|
)
|
|
52
|
-
# Re-point entries' back-reference from Data to the composition
|
|
53
|
-
# record. Entries call `@manifest.policy.*` / `@manifest.resolver.*`
|
|
54
|
-
# at use time (see Entry::Base, Entry::Nested).
|
|
55
|
-
data.entries.each { |e| e.instance_variable_set(:@manifest, composition) }
|
|
56
|
-
composition
|
|
57
52
|
end
|
|
58
53
|
|
|
59
54
|
def check_version!(raw, source)
|
data/lib/textus/mcp/server.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
require "json"
|
|
2
|
-
require "digest"
|
|
3
2
|
|
|
4
3
|
module Textus
|
|
5
4
|
module MCP
|
|
@@ -107,7 +106,7 @@ module Textus
|
|
|
107
106
|
end
|
|
108
107
|
|
|
109
108
|
def manifest_etag
|
|
110
|
-
|
|
109
|
+
@store.file_store.etag(File.join(@store.root, "manifest.yaml"))
|
|
111
110
|
end
|
|
112
111
|
|
|
113
112
|
def emit_result(rid, result)
|
data/lib/textus/mcp/session.rb
CHANGED
|
@@ -23,9 +23,18 @@ module Textus
|
|
|
23
23
|
return if observed_etag == @manifest_etag
|
|
24
24
|
|
|
25
25
|
raise ContractDrift.new(
|
|
26
|
-
"manifest changed (was #{@manifest_etag
|
|
26
|
+
"manifest changed (was #{short_etag(@manifest_etag)}, now #{short_etag(observed_etag)}); re-run boot",
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# First 8 hex chars after the "sha256:" prefix — a stable short id for
|
|
33
|
+
# the drift diagnostic. Tolerates non-prefixed values (delete_prefix is
|
|
34
|
+
# a no-op when the prefix is absent).
|
|
35
|
+
def short_etag(etag)
|
|
36
|
+
etag.to_s.delete_prefix("sha256:")[0, 8]
|
|
37
|
+
end
|
|
29
38
|
end
|
|
30
39
|
end
|
|
31
40
|
end
|
data/lib/textus/mcp/tools.rb
CHANGED
|
@@ -17,11 +17,11 @@ module Textus
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def ops_for(session, store)
|
|
20
|
-
store.
|
|
20
|
+
store.as(session.role)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
REGISTRY = {
|
|
24
|
-
"boot" => ->(_s, store, _a) {
|
|
24
|
+
"boot" => ->(_s, store, _a) { store.boot },
|
|
25
25
|
|
|
26
26
|
"find" => lambda do |s, store, args|
|
|
27
27
|
ops_for(s, store).list(zone: args["zone"], prefix: args["prefix"])
|
data/lib/textus/mcp.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Ports
|
|
5
5
|
# Writes an "event_error" audit row when a user hook raises during
|
|
6
6
|
# Hooks::EventBus publish. Attached at Store boot.
|
|
7
7
|
#
|
|
@@ -11,7 +11,7 @@ module Textus
|
|
|
11
11
|
# event subscribers should be able to filter by key glob).
|
|
12
12
|
#
|
|
13
13
|
# Lifecycle audit rows for verb: "put" / "delete" / "rename" are written
|
|
14
|
-
# by
|
|
14
|
+
# by Envelope::IO::Writer directly (it owns the
|
|
15
15
|
# audit-append-as-final-step invariant); this subscriber covers the
|
|
16
16
|
# hook-failure case the writer never sees.
|
|
17
17
|
class AuditSubscriber
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Ports
|
|
5
5
|
# Publishes built artifacts from the store to repo-relative consumer paths.
|
|
6
6
|
# Publish = copy + sentinel. The in-store file is already the consumer-shaped
|
|
7
7
|
# artifact; no parsing or stripping.
|
|
8
8
|
#
|
|
9
|
-
# Sentinel I/O is delegated to Textus::
|
|
10
|
-
# `<store_root>/sentinels/` and mirror the target's repo-relative layout
|
|
11
|
-
# consumer directories aren't polluted with `.textus-managed.json` siblings.
|
|
9
|
+
# Sentinel I/O is delegated to Textus::Ports::SentinelStore. Sentinels live
|
|
10
|
+
# under `<store_root>/sentinels/` and mirror the target's repo-relative layout
|
|
11
|
+
# so consumer directories aren't polluted with `.textus-managed.json` siblings.
|
|
12
12
|
module Publisher
|
|
13
13
|
def self.publish(source:, target:, store_root:)
|
|
14
14
|
FileUtils.mkdir_p(File.dirname(target))
|
|
15
15
|
refuse_if_unmanaged(target, store_root)
|
|
16
16
|
File.delete(target) if File.symlink?(target)
|
|
17
17
|
FileUtils.cp(source, target)
|
|
18
|
-
Textus::
|
|
18
|
+
Textus::Ports::SentinelStore.new.write!(target: target, source: source, store_root: store_root)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.refuse_if_unmanaged(target, store_root)
|
|
@@ -26,7 +26,7 @@ module Textus
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.managed?(target, store_root)
|
|
29
|
-
File.exist?(Textus::
|
|
29
|
+
File.exist?(Textus::Ports::SentinelStore.new.sentinel_path(target, store_root))
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
module
|
|
2
|
+
module Ports
|
|
3
3
|
module Refresh
|
|
4
4
|
module Detached
|
|
5
5
|
module_function
|
|
@@ -16,12 +16,12 @@ module Textus
|
|
|
16
16
|
$stdout.reopen(File::NULL, "w")
|
|
17
17
|
$stderr.reopen(File::NULL, "w")
|
|
18
18
|
|
|
19
|
-
lock = Textus::
|
|
19
|
+
lock = Textus::Ports::Refresh::Lock.new(root: store_root, key: key)
|
|
20
20
|
exit(0) unless lock.try_acquire
|
|
21
21
|
|
|
22
22
|
begin
|
|
23
23
|
store = Textus::Store.new(store_root)
|
|
24
|
-
store.
|
|
24
|
+
store.as("runner").refresh(key)
|
|
25
25
|
rescue StandardError
|
|
26
26
|
# Already logged via :refresh_failed; exit cleanly.
|
|
27
27
|
ensure
|