textus 0.52.0 → 0.54.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 +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -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 +25 -41
- 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/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- 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 +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- 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/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- 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 +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -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 +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- 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/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- 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/handler_allowlist.rb +0 -17
- 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/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- 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/retention/apply.rb +0 -52
- 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/manifest/entry/validators/events.rb +0 -22
- 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/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- 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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
class Gate
|
|
5
|
+
class Auth
|
|
6
|
+
FLOOR = {
|
|
7
|
+
put: %w[lane_writable_by raw_lane_ingest_only],
|
|
8
|
+
key_delete: %w[lane_deletable_by],
|
|
9
|
+
key_mv: %w[lane_writable_by raw_lane_ingest_only],
|
|
10
|
+
accept: %w[author_held],
|
|
11
|
+
reject: %w[author_held],
|
|
12
|
+
propose: %w[lane_writable_by raw_lane_ingest_only],
|
|
13
|
+
key_mv_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
14
|
+
key_delete_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
15
|
+
ingest: %w[lane_writable_by raw_write_once],
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
AuthContext = Struct.new(
|
|
19
|
+
:actor, :actor_caps, :lane_verb,
|
|
20
|
+
:action, :target, :envelope,
|
|
21
|
+
:mentry, :manifest,
|
|
22
|
+
keyword_init: true
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def initialize(container)
|
|
26
|
+
@manifest = container.manifest
|
|
27
|
+
@schemas = container.schemas
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Command-based check (new Gate path).
|
|
31
|
+
def check!(cmd)
|
|
32
|
+
key = extract_key(cmd)
|
|
33
|
+
return unless key
|
|
34
|
+
|
|
35
|
+
evaluate_predicates(
|
|
36
|
+
action: command_to_action(cmd),
|
|
37
|
+
actor: cmd.role.to_s,
|
|
38
|
+
key: key,
|
|
39
|
+
envelope: nil,
|
|
40
|
+
extra: {},
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Backward-compatible check for inline action auth (accept, put, etc.).
|
|
45
|
+
def check_action!(action:, actor:, key:, envelope: nil, extra: {})
|
|
46
|
+
evaluate_predicates(
|
|
47
|
+
action: action.to_sym,
|
|
48
|
+
actor: actor,
|
|
49
|
+
key: key,
|
|
50
|
+
envelope: envelope,
|
|
51
|
+
extra: extra,
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.command_to_verb
|
|
56
|
+
@command_to_verb ||= Textus::Gate::VERB_COMMAND.invert.freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def command_to_action(cmd)
|
|
62
|
+
self.class.command_to_verb.fetch(cmd.class) do
|
|
63
|
+
raise Textus::UsageError.new("unmapped command: #{cmd.class}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def evaluate_predicates(action:, actor:, key:, envelope:, extra:)
|
|
68
|
+
mentry = @manifest.resolver.resolve(key).entry
|
|
69
|
+
lane_verb = @manifest.policy.verb_for_lane(mentry.lane.to_s)
|
|
70
|
+
actor_caps = Set.new(@manifest.data.role_caps.fetch(actor, []))
|
|
71
|
+
|
|
72
|
+
ctx = AuthContext.new(
|
|
73
|
+
actor:, actor_caps:, lane_verb:,
|
|
74
|
+
action:, target: key, envelope:,
|
|
75
|
+
mentry:, manifest: @manifest
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
failures = []
|
|
79
|
+
floor_preds = FLOOR.fetch(action, [])
|
|
80
|
+
rule_preds = rule_declared_predicates(action, key)
|
|
81
|
+
(floor_preds + rule_preds).uniq.each do |pred|
|
|
82
|
+
result = evaluate(pred, ctx, extra)
|
|
83
|
+
next if result[:pass]
|
|
84
|
+
raise result[:error] if result[:error]
|
|
85
|
+
|
|
86
|
+
failures << [pred, result[:reason]]
|
|
87
|
+
end
|
|
88
|
+
raise Textus::GuardFailed.new(failures) unless failures.empty?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_key(cmd)
|
|
92
|
+
if cmd.respond_to?(:pending_key)
|
|
93
|
+
cmd.pending_key
|
|
94
|
+
elsif cmd.respond_to?(:key)
|
|
95
|
+
cmd.key
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rule_declared_predicates(action, key)
|
|
100
|
+
guard_map = @manifest.rules.for(key).guard
|
|
101
|
+
return [] if guard_map.nil?
|
|
102
|
+
|
|
103
|
+
Array(guard_map[action.to_s])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def evaluate(pred_name, ctx, extra)
|
|
107
|
+
case pred_name
|
|
108
|
+
when "lane_writable_by" then evaluate_lane_writable_by(ctx)
|
|
109
|
+
when "author_held" then evaluate_author_held(ctx)
|
|
110
|
+
when "target_is_canon" then evaluate_target_is_canon(ctx)
|
|
111
|
+
when "etag_match" then evaluate_etag_match(ctx, extra)
|
|
112
|
+
when "schema_valid" then evaluate_schema_valid(ctx)
|
|
113
|
+
when "fresh_within" then { pass: true }
|
|
114
|
+
when "raw_lane_ingest_only" then evaluate_raw_lane_ingest_only(ctx)
|
|
115
|
+
when "raw_write_once" then evaluate_raw_write_once(ctx)
|
|
116
|
+
when "lane_deletable_by" then evaluate_lane_deletable_by(ctx)
|
|
117
|
+
else raise Textus::UsageError.new("unknown predicate '#{pred_name}'")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def evaluate_lane_writable_by(ctx)
|
|
122
|
+
pass = ctx.actor_caps.include?(ctx.lane_verb.to_s)
|
|
123
|
+
return { pass: true } if pass
|
|
124
|
+
|
|
125
|
+
holders = @manifest.policy.roles_with_capability(ctx.lane_verb.to_s)
|
|
126
|
+
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane, verb: ctx.lane_verb, holders:) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def evaluate_author_held(ctx)
|
|
130
|
+
holders = @manifest.policy.roles_with_capability("author")
|
|
131
|
+
pass = holders.include?(ctx.actor.to_s)
|
|
132
|
+
reason = if pass
|
|
133
|
+
nil
|
|
134
|
+
elsif holders.empty?
|
|
135
|
+
"no role holds the 'author' capability; #{ctx.action} is disabled"
|
|
136
|
+
else
|
|
137
|
+
"role '#{ctx.actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
|
|
138
|
+
end
|
|
139
|
+
{ pass:, reason: }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def evaluate_target_is_canon(ctx)
|
|
143
|
+
kind = @manifest.policy.declared_kind(ctx.mentry.lane.to_s)
|
|
144
|
+
pass = kind == :canon
|
|
145
|
+
{ pass:, reason: pass ? nil : "target lane '#{ctx.mentry.lane}' is not canon (kind: #{kind})" }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def evaluate_etag_match(ctx, extra)
|
|
149
|
+
if_etag = extra[:if_etag]
|
|
150
|
+
return { pass: true } if if_etag.nil?
|
|
151
|
+
|
|
152
|
+
current = ctx.envelope&.etag
|
|
153
|
+
pass = current.nil? || current == if_etag
|
|
154
|
+
{ pass:, error: pass ? nil : Textus::EtagMismatch.new(ctx.target, if_etag, current) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def evaluate_schema_valid(ctx)
|
|
158
|
+
return { pass: true } unless ctx.envelope
|
|
159
|
+
|
|
160
|
+
schema_ref = ctx.mentry.schema
|
|
161
|
+
return { pass: true } unless schema_ref
|
|
162
|
+
|
|
163
|
+
schema = @schemas.fetch_or_nil(schema_ref)
|
|
164
|
+
return { pass: true } unless schema
|
|
165
|
+
|
|
166
|
+
frontmatter = ctx.envelope.meta&.dig("_meta") || ctx.envelope.meta || {}
|
|
167
|
+
begin
|
|
168
|
+
schema.validate!(frontmatter)
|
|
169
|
+
{ pass: true }
|
|
170
|
+
rescue Textus::SchemaViolation => e
|
|
171
|
+
{ pass: false, reason: schema_reason(e) }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def evaluate_raw_lane_ingest_only(ctx)
|
|
176
|
+
return { pass: true } unless @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
177
|
+
return { pass: true } if ctx.action == :ingest
|
|
178
|
+
|
|
179
|
+
{ pass: false, error: Textus::Error.new(
|
|
180
|
+
:raw_lane_ingest_only,
|
|
181
|
+
"raw lane '#{ctx.mentry.lane}' only accepts `textus ingest` — " \
|
|
182
|
+
"use that verb instead of '#{ctx.action}'",
|
|
183
|
+
) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def evaluate_raw_write_once(ctx)
|
|
187
|
+
path = @manifest.resolver.resolve(ctx.target).path
|
|
188
|
+
return { pass: true } unless File.exist?(path)
|
|
189
|
+
|
|
190
|
+
{ pass: false, error: Textus::Error.new(
|
|
191
|
+
:raw_write_once,
|
|
192
|
+
"raw entry '#{ctx.target}' already exists; " \
|
|
193
|
+
"delete it first (`textus key-delete #{ctx.target}`), then re-ingest",
|
|
194
|
+
) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Deletion authority: the lane's write capability OR the author capability.
|
|
198
|
+
# On raw-kind lanes only the author capability grants deletion (correction
|
|
199
|
+
# escape hatch); the lane's own verb (ingest) is write-only. On all other
|
|
200
|
+
# lane kinds the behaviour matches lane_writable_by — the lane's writer
|
|
201
|
+
# can delete as before.
|
|
202
|
+
def evaluate_lane_deletable_by(ctx)
|
|
203
|
+
is_raw = @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
204
|
+
pass = if is_raw
|
|
205
|
+
ctx.actor_caps.include?("author")
|
|
206
|
+
else
|
|
207
|
+
ctx.actor_caps.include?(ctx.lane_verb.to_s) || ctx.actor_caps.include?("author")
|
|
208
|
+
end
|
|
209
|
+
return { pass: true } if pass
|
|
210
|
+
|
|
211
|
+
extra_holders = is_raw ? ["author"] : [ctx.lane_verb.to_s, "author"]
|
|
212
|
+
holders = extra_holders.flat_map { |v| @manifest.policy.roles_with_capability(v) }.uniq
|
|
213
|
+
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane,
|
|
214
|
+
verb: ctx.lane_verb, holders:) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def schema_reason(err)
|
|
218
|
+
d = err.details
|
|
219
|
+
return err.message.dup unless d.is_a?(Hash)
|
|
220
|
+
return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
|
|
221
|
+
return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
|
|
222
|
+
|
|
223
|
+
err.message.dup
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
data/lib/textus/gate.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
ingest: Textus::Command::Ingest,
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
ROUTES = {
|
|
38
|
+
Command::Get => [Textus::Action::Get],
|
|
39
|
+
Command::Put => [Textus::Action::Put],
|
|
40
|
+
Command::Propose => [Textus::Action::Propose],
|
|
41
|
+
Command::KeyDelete => [Textus::Action::KeyDelete],
|
|
42
|
+
Command::KeyMv => [Textus::Action::KeyMv],
|
|
43
|
+
Command::Accept => [Textus::Action::Accept],
|
|
44
|
+
Command::Reject => [Textus::Action::Reject],
|
|
45
|
+
Command::Enqueue => [Textus::Action::Enqueue],
|
|
46
|
+
Command::List => [Textus::Action::List],
|
|
47
|
+
Command::Where => [Textus::Action::Where],
|
|
48
|
+
Command::Uid => [Textus::Action::Uid],
|
|
49
|
+
Command::Blame => [Textus::Action::Blame],
|
|
50
|
+
Command::Audit => [Textus::Action::Audit],
|
|
51
|
+
Command::Deps => [Textus::Action::Deps],
|
|
52
|
+
Command::Rdeps => [Textus::Action::Rdeps],
|
|
53
|
+
Command::Pulse => [Textus::Action::Pulse],
|
|
54
|
+
Command::RuleExplain => [Textus::Action::RuleExplain],
|
|
55
|
+
Command::RuleList => [Textus::Action::RuleList],
|
|
56
|
+
Command::RuleLint => [Textus::Action::RuleLint],
|
|
57
|
+
Command::Published => [Textus::Action::Published],
|
|
58
|
+
Command::SchemaShow => [Textus::Action::SchemaEnvelope],
|
|
59
|
+
Command::Doctor => [Textus::Action::Doctor],
|
|
60
|
+
Command::Boot => [Textus::Action::Boot],
|
|
61
|
+
Command::Jobs => [Textus::Action::Jobs],
|
|
62
|
+
Command::DataMv => [Textus::Action::DataMv],
|
|
63
|
+
Command::KeyMvPrefix => [Textus::Action::KeyMvPrefix],
|
|
64
|
+
Command::KeyDeletePrefix => [Textus::Action::KeyDeletePrefix],
|
|
65
|
+
Command::Drain => [Textus::Action::Drain],
|
|
66
|
+
Command::Ingest => [Textus::Action::Ingest],
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
def initialize(container)
|
|
70
|
+
@container = container
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch(cmd, correlation_id: nil)
|
|
74
|
+
cmd = normalize_propose_key(cmd, @container) if cmd.is_a?(Command::Propose)
|
|
75
|
+
action_classes = ROUTES.fetch(cmd.class) do
|
|
76
|
+
raise Textus::UsageError.new("unknown command: #{cmd.class}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Gate::Auth.new(@container).check!(cmd)
|
|
80
|
+
call_obj = build_call(cmd, correlation_id: correlation_id)
|
|
81
|
+
results = action_classes.map { |klass| run_action(klass, cmd, @container, call_obj) }
|
|
82
|
+
results.length == 1 ? results.first : results
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def normalize_propose_key(cmd, container)
|
|
88
|
+
return cmd if cmd.pending_key
|
|
89
|
+
|
|
90
|
+
zone = container.manifest.policy.propose_lane_for(cmd.role.to_s)
|
|
91
|
+
cmd.with(pending_key: zone ? "#{zone}.#{cmd.key}" : nil)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_action(klass, cmd, container, call_obj)
|
|
95
|
+
action = klass.new(**extract_kwargs(klass, cmd))
|
|
96
|
+
action.call(container:, call: call_obj)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def extract_kwargs(klass, cmd)
|
|
100
|
+
params = klass.instance_method(:initialize).parameters
|
|
101
|
+
accepts_keyrest = params.any? { |t, _| t == :keyrest }
|
|
102
|
+
param_set = params.to_set { |_t, n| n }
|
|
103
|
+
cmd.members.each_with_object({}) do |m, h|
|
|
104
|
+
next unless accepts_keyrest || param_set.include?(m)
|
|
105
|
+
|
|
106
|
+
val = cmd.public_send(m)
|
|
107
|
+
h[m] = val unless val.nil?
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_call(cmd, correlation_id: nil)
|
|
112
|
+
dry_run = cmd.respond_to?(:dry_run) ? !cmd.dry_run.nil? : false
|
|
113
|
+
Textus::Call.build(role: cmd.role, dry_run:, correlation_id: correlation_id)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/textus/init.rb
CHANGED
|
@@ -3,7 +3,7 @@ require "pathname"
|
|
|
3
3
|
|
|
4
4
|
module Textus
|
|
5
5
|
module Init
|
|
6
|
-
ZONES = %w[knowledge notebook proposals artifacts].freeze
|
|
6
|
+
ZONES = %w[knowledge notebook proposals artifacts raw].freeze
|
|
7
7
|
|
|
8
8
|
DEFAULT_MANIFEST = <<~YAML
|
|
9
9
|
version: textus/3
|
|
@@ -11,143 +11,97 @@ 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,
|
|
24
|
-
# A per-host snapshot,
|
|
25
|
-
#
|
|
26
|
-
# (see docs/cookbook/environment-scan.md) without renaming. tracked:false →
|
|
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
|
+
# A per-host snapshot, populated by a registered workflow. Nested so it
|
|
25
|
+
# grows to a fleet — add leaves over SSH without renaming. tracked:false →
|
|
27
26
|
# gitignored (machine info can be sensitive/noisy) but still protocol-readable
|
|
28
27
|
# via `textus get artifacts.feeds.machines.local`. Delete to opt out. (ADR 0043)
|
|
29
28
|
- key: artifacts.feeds.machines
|
|
30
|
-
path: artifacts/feeds/machines
|
|
31
|
-
|
|
29
|
+
path: data/artifacts/feeds/machines
|
|
30
|
+
lane: artifacts
|
|
32
31
|
format: yaml
|
|
33
32
|
nested: true
|
|
34
33
|
tracked: false
|
|
35
|
-
kind:
|
|
36
|
-
source:
|
|
37
|
-
from: handler
|
|
38
|
-
handler: machines
|
|
39
|
-
ttl: 1h # cadence on a long-running server
|
|
40
|
-
config:
|
|
41
|
-
machines:
|
|
42
|
-
local: { via: local }
|
|
34
|
+
kind: nested
|
|
43
35
|
rules: []
|
|
44
36
|
YAML
|
|
45
37
|
|
|
46
|
-
HOOKS_README = <<~MD
|
|
47
|
-
# Hooks
|
|
48
|
-
|
|
49
|
-
Drop one Ruby file per hook. All hooks register through one DSL.
|
|
50
|
-
Files anywhere under `.textus/hooks/` (including subdirectories) are loaded at
|
|
51
|
-
startup in alphabetical order by full path. Subdirectory names are organizational
|
|
52
|
-
only — the registered event and name come from the DSL call, not the file path.
|
|
53
|
-
|
|
54
|
-
## DSL
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
Textus.hook do |reg|
|
|
58
|
-
reg.on(:resolve_handler, :my_source) do |config:, args:, **|
|
|
59
|
-
{ _meta: { "last_fetched_at" => Time.now.utc.iso8601 }, body: "…" }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
reg.on(:transform_rows, :my_source) { |rows:, **| rows.map { |r| r.merge(processed: true) } }
|
|
63
|
-
reg.on(:validate, :my_check) { |caps:, **| [] }
|
|
64
|
-
reg.on(:entry_written, :my_listener, keys: ["knowledge.*"]) { |key:, envelope:, **| }
|
|
65
|
-
|
|
66
|
-
# Run a side-effect every time textus writes a file to your repo:
|
|
67
|
-
reg.on(:entry_published, :notify) do |key:, target:, **|
|
|
68
|
-
warn "wrote \#{target} (from \#{key})"
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The intake handler above is paired with a manifest entry whose
|
|
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)
|
|
97
|
-
:entry_written, :entry_deleted, :entry_fetched, :entry_renamed,
|
|
98
|
-
:entry_produced, :produce_failed,
|
|
99
|
-
:proposal_accepted, :proposal_rejected,
|
|
100
|
-
:entry_published, :store_loaded, :session_opened,
|
|
101
|
-
:entry_fetch_started, :entry_fetch_failed (pub-sub — return discarded)
|
|
102
|
-
|
|
103
|
-
See SPEC.md §5.10 for the full table.
|
|
104
|
-
MD
|
|
105
|
-
|
|
106
38
|
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 }
|
|
39
|
+
- { key: knowledge.project, path: data/knowledge/project.md, lane: knowledge, schema: project, owner: human:self, kind: leaf }
|
|
40
|
+
- { key: knowledge.runbooks, path: data/knowledge/runbooks, lane: knowledge, schema: runbook, owner: human:self, nested: true, kind: nested }
|
|
111
41
|
- key: artifacts.derived.orientation
|
|
112
|
-
path: artifacts/derived/orientation.json
|
|
113
|
-
|
|
42
|
+
path: data/artifacts/derived/orientation.json
|
|
43
|
+
lane: artifacts
|
|
114
44
|
publish:
|
|
115
45
|
- { to: CLAUDE.md, template: orientation.mustache, inject_boot: true }
|
|
116
46
|
- { to: AGENTS.md, template: orientation.mustache, inject_boot: true }
|
|
117
|
-
source:
|
|
118
|
-
from: project
|
|
119
|
-
select:
|
|
120
|
-
- knowledge.project
|
|
121
|
-
- knowledge.runbooks
|
|
122
|
-
transform: orientation_reducer
|
|
123
47
|
kind: produced
|
|
124
48
|
YAML
|
|
125
49
|
|
|
126
50
|
def self.run(target_root, with_agent: false)
|
|
51
|
+
check_target!(target_root)
|
|
52
|
+
scaffold_dir = File.expand_path("init/templates", __dir__)
|
|
53
|
+
create_directories(target_root)
|
|
54
|
+
write_manifest(target_root, with_agent:)
|
|
55
|
+
mcp_status = scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
56
|
+
setup_state_dirs(target_root)
|
|
57
|
+
write_gitignore(target_root)
|
|
58
|
+
build_result(target_root, with_agent:, mcp_status:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.check_target!(target_root)
|
|
127
62
|
raise UsageError.new(".textus/ already exists at #{target_root}") if File.directory?(target_root)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
ASSET_ZONES = %w[raw].freeze
|
|
128
66
|
|
|
67
|
+
def self.create_directories(target_root)
|
|
129
68
|
FileUtils.mkdir_p(File.join(target_root, "schemas"))
|
|
130
69
|
FileUtils.mkdir_p(File.join(target_root, "templates"))
|
|
131
|
-
FileUtils.mkdir_p(File.join(target_root, "
|
|
70
|
+
FileUtils.mkdir_p(File.join(target_root, "workflows"))
|
|
132
71
|
ZONES.each do |z|
|
|
133
|
-
dir = File.join(target_root, "
|
|
72
|
+
dir = File.join(target_root, "data", z)
|
|
134
73
|
FileUtils.mkdir_p(dir)
|
|
135
74
|
File.write(File.join(dir, ".gitkeep"), "")
|
|
136
75
|
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
142
|
-
mcp_status = nil
|
|
143
|
-
if with_agent
|
|
144
|
-
scaffold_agent_profile(target_root, scaffold_dir)
|
|
145
|
-
mcp_status = write_mcp_config(target_root, scaffold_dir)
|
|
76
|
+
ASSET_ZONES.each do |z|
|
|
77
|
+
dir = File.join(target_root, "assets", z)
|
|
78
|
+
FileUtils.mkdir_p(dir)
|
|
79
|
+
File.write(File.join(dir, ".gitkeep"), "")
|
|
146
80
|
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.write_manifest(target_root, with_agent:)
|
|
84
|
+
File.write(File.join(target_root, "manifest.yaml"), manifest_yaml(with_agent: with_agent))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.scaffold_agent(target_root, scaffold_dir, with_agent:)
|
|
88
|
+
return nil unless with_agent
|
|
89
|
+
|
|
90
|
+
scaffold_agent_profile(target_root, scaffold_dir)
|
|
91
|
+
write_mcp_config(target_root, scaffold_dir)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.setup_state_dirs(target_root)
|
|
147
95
|
FileUtils.mkdir_p(Textus::Layout.audit_dir(target_root))
|
|
148
96
|
FileUtils.mkdir_p(Textus::Layout.state(target_root))
|
|
149
97
|
FileUtils.mkdir_p(Textus::Layout.locks(target_root))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.write_gitignore(target_root)
|
|
150
101
|
File.write(File.join(target_root, ".gitignore"), derived_gitignore(target_root))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.build_result(target_root, with_agent:, mcp_status:)
|
|
151
105
|
result = { "protocol" => PROTOCOL, "initialized" => target_root, "profile" => with_agent ? "agent" : "default" }
|
|
152
106
|
result["mcp_config"] = mcp_status if with_agent
|
|
153
107
|
result
|
|
@@ -168,7 +122,6 @@ module Textus
|
|
|
168
122
|
"project.schema.yaml" => File.join("schemas", "project.yaml"),
|
|
169
123
|
"runbook.schema.yaml" => File.join("schemas", "runbook.yaml"),
|
|
170
124
|
"orientation.mustache" => File.join("templates", "orientation.mustache"),
|
|
171
|
-
"orientation_reducer.rb" => File.join("hooks", "orientation_reducer.rb"),
|
|
172
125
|
}.each do |src, dest|
|
|
173
126
|
File.write(File.join(target_root, dest), File.read(File.join(scaffold_dir, src)))
|
|
174
127
|
end
|
|
@@ -193,8 +146,9 @@ module Textus
|
|
|
193
146
|
manifest = Textus::Manifest.load(target_root)
|
|
194
147
|
root = Pathname.new(target_root)
|
|
195
148
|
untracked = manifest.data.entries.reject(&:tracked?).map do |e|
|
|
196
|
-
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* →
|
|
197
|
-
"
|
|
149
|
+
if e.nested? # a whole subtree of leaf files (artifacts.feeds.machines.* → data/artifacts/feeds/machines/)
|
|
150
|
+
rel = e.path.start_with?("data/") ? e.path : File.join("data", e.path)
|
|
151
|
+
"#{rel}/"
|
|
198
152
|
else
|
|
199
153
|
Pathname.new(Textus::Key::Path.resolve(manifest.data, e)).relative_path_from(root).to_s
|
|
200
154
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Jobs
|
|
3
|
+
class Base
|
|
4
|
+
def self.inherited(subclass)
|
|
5
|
+
super
|
|
6
|
+
return unless subclass.name
|
|
7
|
+
|
|
8
|
+
TracePoint.new(:end) do |tp|
|
|
9
|
+
if tp.self == subclass
|
|
10
|
+
Textus::Jobs.register(subclass)
|
|
11
|
+
tp.disable
|
|
12
|
+
end
|
|
13
|
+
end.enable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(**)
|
|
17
|
+
raise NotImplementedError.new("#{self.class}#call")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def args = {}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Jobs
|
|
5
|
+
class Materialize < Base
|
|
6
|
+
TYPE = "materialize"
|
|
7
|
+
|
|
8
|
+
def initialize(key:)
|
|
9
|
+
super()
|
|
10
|
+
@key = key
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def args = { key: @key }
|
|
14
|
+
|
|
15
|
+
def call(container:, call:)
|
|
16
|
+
Textus::Produce::Engine.converge(container: container, call: call, keys: [@key])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|