textus 0.52.0 → 0.53.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/CHANGELOG.md +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +11 -11
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
data/lib/textus/gate.rb
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Gate
|
|
5
|
+
VERB_COMMAND = {
|
|
6
|
+
get: Textus::Command::Get,
|
|
7
|
+
put: Textus::Command::Put,
|
|
8
|
+
propose: Textus::Command::Propose,
|
|
9
|
+
key_delete: Textus::Command::KeyDelete,
|
|
10
|
+
key_mv: Textus::Command::KeyMv,
|
|
11
|
+
accept: Textus::Command::Accept,
|
|
12
|
+
reject: Textus::Command::Reject,
|
|
13
|
+
enqueue: Textus::Command::Enqueue,
|
|
14
|
+
list: Textus::Command::List,
|
|
15
|
+
where: Textus::Command::Where,
|
|
16
|
+
uid: Textus::Command::Uid,
|
|
17
|
+
blame: Textus::Command::Blame,
|
|
18
|
+
audit: Textus::Command::Audit,
|
|
19
|
+
deps: Textus::Command::Deps,
|
|
20
|
+
rdeps: Textus::Command::Rdeps,
|
|
21
|
+
pulse: Textus::Command::Pulse,
|
|
22
|
+
rule_explain: Textus::Command::RuleExplain,
|
|
23
|
+
rule_list: Textus::Command::RuleList,
|
|
24
|
+
rule_lint: Textus::Command::RuleLint,
|
|
25
|
+
published: Textus::Command::Published,
|
|
26
|
+
schema_show: Textus::Command::SchemaShow,
|
|
27
|
+
doctor: Textus::Command::Doctor,
|
|
28
|
+
boot: Textus::Command::Boot,
|
|
29
|
+
jobs: Textus::Command::Jobs,
|
|
30
|
+
data_mv: Textus::Command::DataMv,
|
|
31
|
+
key_mv_prefix: Textus::Command::KeyMvPrefix,
|
|
32
|
+
key_delete_prefix: Textus::Command::KeyDeletePrefix,
|
|
33
|
+
drain: Textus::Command::Drain,
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
ROUTES = {
|
|
37
|
+
Command::Get => [Textus::Action::Get],
|
|
38
|
+
Command::Put => [Textus::Action::Put],
|
|
39
|
+
Command::Propose => [Textus::Action::Propose],
|
|
40
|
+
Command::KeyDelete => [Textus::Action::KeyDelete],
|
|
41
|
+
Command::KeyMv => [Textus::Action::KeyMv],
|
|
42
|
+
Command::Accept => [Textus::Action::Accept],
|
|
43
|
+
Command::Reject => [Textus::Action::Reject],
|
|
44
|
+
Command::Enqueue => [Textus::Action::Enqueue],
|
|
45
|
+
Command::List => [Textus::Action::List],
|
|
46
|
+
Command::Where => [Textus::Action::Where],
|
|
47
|
+
Command::Uid => [Textus::Action::Uid],
|
|
48
|
+
Command::Blame => [Textus::Action::Blame],
|
|
49
|
+
Command::Audit => [Textus::Action::Audit],
|
|
50
|
+
Command::Deps => [Textus::Action::Deps],
|
|
51
|
+
Command::Rdeps => [Textus::Action::Rdeps],
|
|
52
|
+
Command::Pulse => [Textus::Action::Pulse],
|
|
53
|
+
Command::RuleExplain => [Textus::Action::RuleExplain],
|
|
54
|
+
Command::RuleList => [Textus::Action::RuleList],
|
|
55
|
+
Command::RuleLint => [Textus::Action::RuleLint],
|
|
56
|
+
Command::Published => [Textus::Action::Published],
|
|
57
|
+
Command::SchemaShow => [Textus::Action::SchemaEnvelope],
|
|
58
|
+
Command::Doctor => [Textus::Action::Doctor],
|
|
59
|
+
Command::Boot => [Textus::Action::Boot],
|
|
60
|
+
Command::Jobs => [Textus::Action::Jobs],
|
|
61
|
+
Command::DataMv => [Textus::Action::DataMv],
|
|
62
|
+
Command::KeyMvPrefix => [Textus::Action::KeyMvPrefix],
|
|
63
|
+
Command::KeyDeletePrefix => [Textus::Action::KeyDeletePrefix],
|
|
64
|
+
Command::Drain => [Textus::Action::Drain],
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
def initialize(container)
|
|
68
|
+
@container = container
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def dispatch(cmd, correlation_id: nil)
|
|
72
|
+
cmd = normalize_propose_key(cmd, @container) if cmd.is_a?(Command::Propose)
|
|
73
|
+
action_classes = ROUTES.fetch(cmd.class) do
|
|
74
|
+
raise Textus::UsageError.new("unknown command: #{cmd.class}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Gate::Auth.new(@container).check!(cmd)
|
|
78
|
+
call_obj = build_call(cmd, correlation_id: correlation_id)
|
|
79
|
+
results = action_classes.map { |klass| run_action(klass, cmd, @container, call_obj) }
|
|
80
|
+
results.length == 1 ? results.first : results
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def normalize_propose_key(cmd, container)
|
|
86
|
+
return cmd if cmd.pending_key
|
|
87
|
+
|
|
88
|
+
zone = container.manifest.policy.propose_lane_for(cmd.role.to_s)
|
|
89
|
+
cmd.with(pending_key: zone ? "#{zone}.#{cmd.key}" : nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run_action(klass, cmd, container, call_obj)
|
|
93
|
+
action = klass.new(**extract_kwargs(klass, cmd))
|
|
94
|
+
action.call(container:, call: call_obj)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_kwargs(klass, cmd)
|
|
98
|
+
params = klass.instance_method(:initialize).parameters
|
|
99
|
+
accepts_keyrest = params.any? { |t, _| t == :keyrest }
|
|
100
|
+
param_set = params.to_set { |_t, n| n }
|
|
101
|
+
cmd.members.each_with_object({}) do |m, h|
|
|
102
|
+
next unless accepts_keyrest || param_set.include?(m)
|
|
103
|
+
|
|
104
|
+
val = cmd.public_send(m)
|
|
105
|
+
h[m] = val unless val.nil?
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_call(cmd, correlation_id: nil)
|
|
110
|
+
dry_run = cmd.respond_to?(:dry_run) ? !cmd.dry_run.nil? : false
|
|
111
|
+
Textus::Call.build(role: cmd.role, dry_run:, correlation_id: correlation_id)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# .textus/
|
|
1
|
+
# .textus/steps/fetch/machine_intake.rb
|
|
2
2
|
# Scaffolded by `textus init` — CUSTOMIZE FREELY, or delete the feeds.machines
|
|
3
3
|
# entry from manifest.yaml if you don't want it.
|
|
4
4
|
# Feeds a per-host SNAPSHOT into feeds.machines.<host> on `textus drain`
|
|
@@ -6,40 +6,44 @@
|
|
|
6
6
|
# `local` leaf scans THIS host; add ssh hosts with the cookbook recipe
|
|
7
7
|
# (docs/cookbook/environment-scan.md). tracked:false → gitignored. Keep this an
|
|
8
8
|
# ALLOWLIST of versions and counts — NEVER secrets, raw `env`, or package lists.
|
|
9
|
-
Textus
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
module Textus
|
|
10
|
+
module Step
|
|
11
|
+
class MachineIntakeFetch < Fetch
|
|
12
|
+
def call(config:, args:, caps:, **)
|
|
13
|
+
machine = args[:leaf_segments].first or
|
|
14
|
+
raise "machines intake needs a host leaf, e.g. the 'local' in feeds.machines.local"
|
|
15
|
+
spec = (config["machines"] || {}).fetch(machine) { raise "unknown machine: #{machine}" }
|
|
16
|
+
unless (spec["via"] || "local").to_s == "local"
|
|
17
|
+
raise "machine #{machine}: only `via: local` is scaffolded — see " \
|
|
18
|
+
"docs/cookbook/environment-scan.md for the SSH (remote) fan-out"
|
|
19
|
+
end
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
21
|
+
sh = ->(cmd) { `#{cmd}`.strip } # local shell-out, no network
|
|
22
|
+
ver = ->(cmd) { o = `#{cmd} 2>/dev/null`.strip; o.empty? ? nil : o } # nil if tool absent
|
|
23
|
+
count = ->(cmd) { n = `#{cmd} 2>/dev/null`.strip.lines.size; n.zero? ? nil : n }
|
|
24
|
+
{ content: {
|
|
25
|
+
# git_* describe THIS repo on the control host — only meaningful for `local`.
|
|
26
|
+
"git_head" => sh.call("git rev-parse --short HEAD 2>/dev/null"),
|
|
27
|
+
"git_branch" => sh.call("git rev-parse --abbrev-ref HEAD 2>/dev/null"),
|
|
28
|
+
"git_dirty" => !sh.call("git status --porcelain 2>/dev/null").empty?,
|
|
29
|
+
"repo_root" => sh.call("git rev-parse --show-toplevel 2>/dev/null"),
|
|
30
|
+
"captured_at" => Time.now.utc.iso8601,
|
|
31
|
+
"os" => RbConfig::CONFIG["host_os"],
|
|
32
|
+
"arch" => RbConfig::CONFIG["host_cpu"],
|
|
33
|
+
"ruby_version" => RUBY_VERSION,
|
|
34
|
+
"runtimes" => { # versions only; nil when not installed
|
|
35
|
+
"node" => ver.call("node --version"),
|
|
36
|
+
"python" => ver.call("python3 --version"),
|
|
37
|
+
"go" => ver.call("go version"),
|
|
38
|
+
},
|
|
39
|
+
"packages" => { # COUNTS only — never the list (size/secrets)
|
|
40
|
+
"brew" => count.call("brew list --formula"), # ~1-3s on macOS; runs only on fetch, amortized by the ttl rule
|
|
41
|
+
"apt" => count.call("dpkg-query -f '.\n' -W"),
|
|
42
|
+
},
|
|
43
|
+
"textus_version" => Textus::VERSION,
|
|
44
|
+
"protocol" => Textus::PROTOCOL,
|
|
45
|
+
} }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
end
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# Reducer that reshapes the raw projection rows into the keys the
|
|
2
2
|
# orientation.mustache template references. Without this, the template
|
|
3
3
|
# would only have access to the flat rows list.
|
|
4
|
-
Textus
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
module Textus
|
|
5
|
+
module Step
|
|
6
|
+
class OrientationTransform < Transform
|
|
7
|
+
def call(rows:, config:, **)
|
|
8
|
+
project_row = rows.find { |r| r["_key"] == "knowledge.project" } || {}
|
|
9
|
+
runbook_rows = rows.select { |r| r["_key"]&.start_with?("knowledge.runbooks.") }
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
{
|
|
12
|
+
"project" => {
|
|
13
|
+
"name" => project_row["name"],
|
|
14
|
+
"description" => project_row["description"]
|
|
15
|
+
},
|
|
16
|
+
"runbooks" => runbook_rows.map { |r| { "name" => r["name"], "description" => r["description"] } }
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
16
20
|
end
|
|
17
21
|
end
|
data/lib/textus/init.rb
CHANGED
|
@@ -11,31 +11,31 @@ module Textus
|
|
|
11
11
|
- { name: human, can: [author, propose] }
|
|
12
12
|
- { name: agent, can: [propose, keep] }
|
|
13
13
|
- { name: automation, can: [converge] }
|
|
14
|
-
|
|
14
|
+
lanes:
|
|
15
15
|
- { name: knowledge, kind: canon, desc: "the maintained source of truth (identity.* lives here)" }
|
|
16
16
|
- { name: notebook, kind: workspace, owner: agent, desc: "the agent's own durable working notes" }
|
|
17
17
|
- { name: proposals, kind: queue, desc: "changes awaiting your accept" }
|
|
18
18
|
- { name: artifacts, kind: machine, desc: "machine-maintained: external inputs (artifacts.feeds.*) + computed outputs (artifacts.derived.*)" }
|
|
19
19
|
entries:
|
|
20
|
-
- { key: knowledge.identity, path: knowledge/identity.md,
|
|
21
|
-
- { key: knowledge.notes, path: knowledge/notes,
|
|
22
|
-
- { key: notebook.notes, path: notebook/notes,
|
|
23
|
-
- { key: proposals.notes, path: proposals/notes,
|
|
20
|
+
- { key: knowledge.identity, path: data/knowledge/identity.md, lane: knowledge, schema: null, owner: human:self, kind: leaf }
|
|
21
|
+
- { key: knowledge.notes, path: data/knowledge/notes, lane: knowledge, schema: null, owner: human:self, nested: true, kind: nested }
|
|
22
|
+
- { key: notebook.notes, path: data/notebook/notes, lane: notebook, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
23
|
+
- { key: proposals.notes, path: data/proposals/notes, lane: proposals, schema: null, owner: agent:self, nested: true, kind: nested }
|
|
24
24
|
# A per-host snapshot, refreshed from its declared intake by `textus drain` (scheduled, or on demand).
|
|
25
25
|
# Nested so it grows to a fleet — add artifacts.feeds.machines.<host> leaves over SSH
|
|
26
26
|
# (see docs/cookbook/environment-scan.md) without renaming. tracked:false →
|
|
27
27
|
# gitignored (machine info can be sensitive/noisy) but still protocol-readable
|
|
28
28
|
# via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
|
|
29
29
|
- key: artifacts.feeds.machines
|
|
30
|
-
path: artifacts/feeds/machines
|
|
31
|
-
|
|
30
|
+
path: data/artifacts/feeds/machines
|
|
31
|
+
lane: artifacts
|
|
32
32
|
format: yaml
|
|
33
33
|
nested: true
|
|
34
34
|
tracked: false
|
|
35
35
|
kind: produced
|
|
36
36
|
source:
|
|
37
|
-
from:
|
|
38
|
-
handler:
|
|
37
|
+
from: fetch
|
|
38
|
+
handler: machine-intake
|
|
39
39
|
ttl: 1h # cadence on a long-running server
|
|
40
40
|
config:
|
|
41
41
|
machines:
|
|
@@ -43,57 +43,40 @@ module Textus
|
|
|
43
43
|
rules: []
|
|
44
44
|
YAML
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
#
|
|
46
|
+
STEPS_README = <<~MD
|
|
47
|
+
# Steps
|
|
48
48
|
|
|
49
|
-
Drop one Ruby file per
|
|
50
|
-
Files
|
|
51
|
-
startup
|
|
52
|
-
only — the registered event and name come from the DSL call, not the file path.
|
|
49
|
+
Drop one Ruby file per step. Steps are discovered by convention.
|
|
50
|
+
Files under `.textus/steps/<kind>/<name>.rb` are loaded at
|
|
51
|
+
startup and registered.
|
|
53
52
|
|
|
54
|
-
##
|
|
53
|
+
## Conventions
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
The directory name (`<kind>`) must be one of:
|
|
56
|
+
- `fetch`: Acquires data from outside the store.
|
|
57
|
+
- `transform`: Reshapes projection rows.
|
|
58
|
+
- `validate`: Validates data before writing.
|
|
59
|
+
- `observe`: Listens to store events.
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
The filename (`<name>.rb`) defines the step name. The class defined
|
|
62
|
+
in the file must be a subclass of `Textus::Step::<Kind>` (e.g.
|
|
63
|
+
`Textus::Step::Fetch`) and be wrapped in the `Textus::Step` module.
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
module Textus
|
|
69
|
+
module Step
|
|
70
|
+
class MyFetch < Fetch
|
|
71
|
+
def call(config:, args:, caps:, **)
|
|
72
|
+
{ content: { "foo" => "bar" } }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
`source:` block declares the handler and its refresh cadence
|
|
75
|
-
(`ttl`). Age GC (drop/archive) lives in a top-level `retention:`
|
|
76
|
-
rule, not on the entry:
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
79
|
-
entries:
|
|
80
|
-
- key: artifacts.feeds.foo
|
|
81
|
-
kind: produced
|
|
82
|
-
path: artifacts/feeds/foo.md
|
|
83
|
-
zone: artifacts
|
|
84
|
-
source:
|
|
85
|
-
from: handler
|
|
86
|
-
handler: my_source
|
|
87
|
-
ttl: 10m # refresh cadence for this intake
|
|
88
|
-
|
|
89
|
-
rules:
|
|
90
|
-
- match: artifacts.feeds.foo
|
|
91
|
-
retention:
|
|
92
|
-
ttl: 30d
|
|
93
|
-
action: archive # drop | archive (age GC of stored rows)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Events: :resolve_handler, :transform_rows, :validate (rpc — return value used)
|
|
79
|
+
Events: :fetch, :transform, :validate (rpc — return value used)
|
|
97
80
|
:entry_written, :entry_deleted, :entry_fetched, :entry_renamed,
|
|
98
81
|
:entry_produced, :produce_failed,
|
|
99
82
|
:proposal_accepted, :proposal_rejected,
|
|
@@ -104,50 +87,83 @@ module Textus
|
|
|
104
87
|
MD
|
|
105
88
|
|
|
106
89
|
AGENT_ENTRIES = <<~YAML.gsub(/^/, " ")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- { key: knowledge.project, path: knowledge/project.md, zone: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
110
|
-
- { key: knowledge.runbooks, path: knowledge/runbooks, zone: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
90
|
+
- { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
91
|
+
- { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
111
92
|
- key: artifacts.derived.orientation
|
|
112
|
-
path: artifacts/derived/orientation.json
|
|
113
|
-
|
|
93
|
+
path: data/artifacts/derived/orientation.json
|
|
94
|
+
lane: artifacts
|
|
114
95
|
publish:
|
|
115
96
|
- { to: CLAUDE.md, template: orientation.mustache, inject_boot: true }
|
|
116
97
|
- { to: AGENTS.md, template: orientation.mustache, inject_boot: true }
|
|
117
98
|
source:
|
|
118
|
-
from:
|
|
99
|
+
from: derive
|
|
119
100
|
select:
|
|
120
101
|
- knowledge.project
|
|
121
102
|
- knowledge.runbooks
|
|
122
|
-
transform:
|
|
103
|
+
transform: orientation
|
|
123
104
|
kind: produced
|
|
124
105
|
YAML
|
|
125
106
|
|
|
126
107
|
def self.run(target_root, with_agent: false)
|
|
108
|
+
check_target!(target_root)
|
|
109
|
+
scaffold_dir = File.expand_path("init/templates", __dir__)
|
|
110
|
+
create_directories(target_root)
|
|
111
|
+
write_steps_readme(target_root, scaffold_dir)
|
|
112
|
+
write_manifest(target_root, with_agent:)
|
|
113
|
+
mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
114
|
+
setup_state_dirs(target_root)
|
|
115
|
+
write_gitignore(target_root)
|
|
116
|
+
build_result(target_root, with_agent:, mcp_status:)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.check_target!(target_root)
|
|
127
120
|
raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
|
|
121
|
+
end
|
|
128
122
|
|
|
123
|
+
def self.create_directories(target_root)
|
|
129
124
|
FileUtils.mkdir_p(File.join(target_root, "schemas"))
|
|
130
125
|
FileUtils.mkdir_p(File.join(target_root, "templates"))
|
|
131
|
-
FileUtils.mkdir_p(File.join(target_root, "
|
|
126
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/fetch"))
|
|
127
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/transform"))
|
|
128
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/validate"))
|
|
129
|
+
FileUtils.mkdir_p(File.join(target_root, "steps/observe"))
|
|
132
130
|
ZONES.each do |z|
|
|
133
|
-
dir = File.join(target_root, "
|
|
131
|
+
dir = File.join(target_root, "data", z)
|
|
134
132
|
FileUtils.mkdir_p(dir)
|
|
135
133
|
File.write(File.join(dir, ".gitkeep"), "")
|
|
136
134
|
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.write_steps_readme(target_root, scaffold_dir)
|
|
138
|
+
File.write(File.join(target_root, "steps/README.md"), STEPS_README)
|
|
139
|
+
File.write(
|
|
140
|
+
File.join(target_root, "steps/fetch/machine-intake.rb"),
|
|
141
|
+
File.read(File.join(scaffold_dir, "machine_intake.rb")),
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.write_manifest(target_root, with_agent:)
|
|
141
146
|
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
150
|
+
return nil unless with_agent
|
|
151
|
+
|
|
152
|
+
scaffold_agent_profile(target_root, scaffold_dir)
|
|
153
|
+
write_mcp_config(target_root, scaffold_dir)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.setup_state_dirs(target_root)
|
|
147
157
|
FileUtils.mkdir_p(Textus::Layout.audit_dir(target_root))
|
|
148
158
|
FileUtils.mkdir_p(Textus::Layout.state(target_root))
|
|
149
159
|
FileUtils.mkdir_p(Textus::Layout.locks(target_root))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.write_gitignore(target_root)
|
|
150
163
|
File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.build_result(target_root, with_agent:, mcp_status:)
|
|
151
167
|
result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
|
|
152
168
|
result["mcp_config"] = mcp_status if with_agent
|
|
153
169
|
result
|
|
@@ -168,7 +184,7 @@ module Textus
|
|
|
168
184
|
"project.schema.yaml" => File.join("schemas", "project.yaml"),
|
|
169
185
|
"runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
|
|
170
186
|
"orientation.mustache" => File.join("templates", "orientation.mustache"),
|
|
171
|
-
"orientation_reducer.rb" => File.join("
|
|
187
|
+
"orientation_reducer.rb" => File.join("steps/transform", "orientation.rb"),
|
|
172
188
|
}.each do |src, dest|
|
|
173
189
|
File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
|
|
174
190
|
end
|
|
@@ -193,8 +209,9 @@ module Textus
|
|
|
193
209
|
manifest = Textus::Manifest.load(target_root)
|
|
194
210
|
root = Pathname.new(target_root)
|
|
195
211
|
untracked = manifest.data.entries.reject(&:tracked?).map do |e|
|
|
196
|
-
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* →
|
|
197
|
-
"
|
|
212
|
+
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
|
|
213
|
+
rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
|
|
214
|
+
"#{rel}/"
|
|
198
215
|
else
|
|
199
216
|
Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
|
|
200
217
|
end
|
data/lib/textus/key/path.rb
CHANGED
|
@@ -10,12 +10,19 @@ module Textus
|
|
|
10
10
|
# `manifest.data`.
|
|
11
11
|
def self.resolve(data, mentry)
|
|
12
12
|
primary_ext = Entry.for_format(mentry.format).extensions.first
|
|
13
|
+
rel_path = normalize_relative_path(mentry.path)
|
|
13
14
|
if File.extname(mentry.path) == ""
|
|
14
|
-
File.join(data.root,
|
|
15
|
+
File.join(data.root, rel_path + primary_ext)
|
|
15
16
|
else
|
|
16
|
-
File.join(data.root,
|
|
17
|
+
File.join(data.root, rel_path)
|
|
17
18
|
end
|
|
18
19
|
end
|
|
20
|
+
|
|
21
|
+
def self.normalize_relative_path(path)
|
|
22
|
+
return path if path.start_with?("data/")
|
|
23
|
+
|
|
24
|
+
File.join("data", path)
|
|
25
|
+
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
data/lib/textus/layout.rb
CHANGED
|
@@ -4,6 +4,15 @@ module Textus
|
|
|
4
4
|
# tracked/disposable boundary is a directory boundary. ADR 0038.
|
|
5
5
|
module Layout
|
|
6
6
|
RUN = ".run"
|
|
7
|
+
DATA = "data"
|
|
8
|
+
|
|
9
|
+
def self.data(root)
|
|
10
|
+
File.join(root, DATA)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.data_lane(root, lane_name)
|
|
14
|
+
File.join(data(root), lane_name)
|
|
15
|
+
end
|
|
7
16
|
|
|
8
17
|
def self.run(root)
|
|
9
18
|
File.join(root, RUN)
|
|
@@ -25,6 +34,10 @@ module Textus
|
|
|
25
34
|
File.join(run(root), "build.lock")
|
|
26
35
|
end
|
|
27
36
|
|
|
37
|
+
def self.watcher_lock(root)
|
|
38
|
+
File.join(run(root), "watcher.lock")
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
def self.queue(root)
|
|
29
42
|
File.join(run(root), "queue")
|
|
30
43
|
end
|
data/lib/textus/manifest/data.rb
CHANGED
|
@@ -5,15 +5,15 @@ module Textus
|
|
|
5
5
|
class Manifest
|
|
6
6
|
# Immutable, parsed view of a manifest YAML document.
|
|
7
7
|
#
|
|
8
|
-
# Holds raw structural data (
|
|
9
|
-
# but no behaviour beyond accessors. Behaviour (
|
|
8
|
+
# Holds raw structural data (lanes, entries, audit_config, role_caps)
|
|
9
|
+
# but no behaviour beyond accessors. Behaviour (lane authority, key
|
|
10
10
|
# resolution, rules) lives on Manifest::Policy / Resolver / Rules.
|
|
11
11
|
class Data
|
|
12
12
|
AUDIT_DEFAULTS = { max_size: 10_485_760, keep: 5 }.freeze
|
|
13
13
|
WORKER_DEFAULTS = { pool: 4, poll: 5, lease_ttl: 60, max_attempts: 3 }.freeze
|
|
14
14
|
|
|
15
|
-
attr_reader :raw, :root, :entries, :
|
|
16
|
-
:
|
|
15
|
+
attr_reader :raw, :root, :entries, :declared_lane_kinds,
|
|
16
|
+
:lane_descs, :lane_owners,
|
|
17
17
|
:audit_config, :worker_config, :role_caps, :policy
|
|
18
18
|
|
|
19
19
|
def self.validate_key!(key)
|
|
@@ -27,7 +27,7 @@ module Textus
|
|
|
27
27
|
def validate_key!(key) = self.class.validate_key!(key)
|
|
28
28
|
|
|
29
29
|
def self.parse(raw, root:)
|
|
30
|
-
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare
|
|
30
|
+
raise BadFrontmatter.new(File.join(root.to_s, "manifest.yaml"), "manifest must declare lanes:") if Array(raw["lanes"]).empty?
|
|
31
31
|
|
|
32
32
|
Schema.validate!(raw)
|
|
33
33
|
new(raw: raw, root: root)
|
|
@@ -36,17 +36,17 @@ module Textus
|
|
|
36
36
|
def initialize(raw:, root:)
|
|
37
37
|
@raw = raw
|
|
38
38
|
@root = root
|
|
39
|
-
# Write authority is derived from capabilities ×
|
|
40
|
-
# not a per-
|
|
41
|
-
# one kind-keyed map below (
|
|
42
|
-
# read-side callers (boot, maintenance/
|
|
43
|
-
@
|
|
39
|
+
# Write authority is derived from capabilities × lane-kind (ADR 0030),
|
|
40
|
+
# not a per-lane writer list. "Which lanes are declared" lives in the
|
|
41
|
+
# one kind-keyed map below (declared_lane_kinds); membership checks by
|
|
42
|
+
# read-side callers (boot, maintenance/data_mv) use its keyset (ADR 0034).
|
|
43
|
+
@declared_lane_kinds = Array(raw["lanes"]).to_h do |z|
|
|
44
44
|
[z["name"], z["kind"]&.to_sym]
|
|
45
45
|
end
|
|
46
|
-
@
|
|
47
|
-
# Only
|
|
48
|
-
# future `
|
|
49
|
-
@
|
|
46
|
+
@lane_descs = Array(raw["lanes"]).to_h { |z| [z["name"], z["desc"]] }
|
|
47
|
+
# Only lanes that actually declare an owner — keep nil-tombstones out so a
|
|
48
|
+
# future `lane_owners.key?(name)` means "owner declared", not "lane exists".
|
|
49
|
+
@lane_owners = Array(raw["lanes"]).to_h { |z| [z["name"], z["owner"]] }.compact
|
|
50
50
|
@audit_config = build_audit_config(raw)
|
|
51
51
|
@worker_config = build_worker_config(raw)
|
|
52
52
|
@role_caps = Capabilities.resolve(raw["roles"])
|