textus 0.54.2 → 0.55.2
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 +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
data/lib/textus/action/key_mv.rb
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyMv < WriteVerb
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :key_mv
|
|
9
|
-
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "key mv"
|
|
12
|
-
arg :old_key, String, required: true, positional: true,
|
|
13
|
-
description: "current dotted key"
|
|
14
|
-
arg :new_key, String, required: true, positional: true,
|
|
15
|
-
description: "new dotted key (must be the same zone and format as old_key)"
|
|
16
|
-
arg :dry_run, :boolean,
|
|
17
|
-
description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
|
|
18
|
-
"defaults to false, so omitting it applies the move immediately " \
|
|
19
|
-
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
20
|
-
|
|
21
|
-
def initialize(old_key:, new_key:, dry_run: false)
|
|
22
|
-
super()
|
|
23
|
-
@old_key = old_key
|
|
24
|
-
@new_key = new_key
|
|
25
|
-
@dry_run = dry_run
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(container:, call:)
|
|
29
|
-
run_with_cascade(cascade_target_key, container:, call:) do
|
|
30
|
-
execute_move(container, call)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def cascade_target_key
|
|
37
|
-
@dry_run ? nil : @new_key
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def execute_move(container, call)
|
|
41
|
-
old_res, new_res = prepare(container, call)
|
|
42
|
-
return dry_run_result(container, old_res, new_res) if @dry_run
|
|
43
|
-
|
|
44
|
-
envelope = apply_move(container, call, old_res, new_res)
|
|
45
|
-
success_result(old_res, new_res, envelope)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def apply_move(container, call, old_res, new_res)
|
|
49
|
-
ensure_uid!(container, call, old_res.entry)
|
|
50
|
-
writer(container, call).move(
|
|
51
|
-
from_key: @old_key,
|
|
52
|
-
to_key: @new_key,
|
|
53
|
-
new_mentry: new_res.entry,
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def success_result(old_res, new_res, envelope)
|
|
58
|
-
{
|
|
59
|
-
"protocol" => PROTOCOL,
|
|
60
|
-
"ok" => true,
|
|
61
|
-
"from_key" => @old_key,
|
|
62
|
-
"to_key" => @new_key,
|
|
63
|
-
"from_path" => old_res.path,
|
|
64
|
-
"to_path" => new_res.path,
|
|
65
|
-
"uid" => envelope.uid,
|
|
66
|
-
"envelope" => envelope.to_h_for_wire,
|
|
67
|
-
}
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def prepare(container, call)
|
|
71
|
-
Textus::Manifest::Data.validate_key!(@old_key)
|
|
72
|
-
Textus::Manifest::Data.validate_key!(@new_key)
|
|
73
|
-
raise UsageError.new("mv: old and new keys are identical") if @old_key == @new_key
|
|
74
|
-
|
|
75
|
-
old_res = container.manifest.resolver.resolve(@old_key)
|
|
76
|
-
new_res = container.manifest.resolver.resolve(@new_key)
|
|
77
|
-
raise UnknownKey.new(@old_key) unless reader(container).exists?(@old_key)
|
|
78
|
-
|
|
79
|
-
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
80
|
-
auth(container).check_action!(action: :key_mv, actor: call.role, key: @old_key)
|
|
81
|
-
auth(container).check_action!(action: :key_mv, actor: call.role, key: @new_key)
|
|
82
|
-
raise UsageError.new("mv: target '#{@new_key}' already exists at #{new_res.path}") if reader(container).exists?(@new_key)
|
|
83
|
-
|
|
84
|
-
[old_res, new_res]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_zone_and_format!(old_mentry, new_mentry)
|
|
88
|
-
if old_mentry.lane != new_mentry.lane
|
|
89
|
-
raise UsageError.new(
|
|
90
|
-
"mv: cross-zone move refused (#{old_mentry.lane} -> #{new_mentry.lane}). " \
|
|
91
|
-
"Use put+delete for cross-zone moves.",
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
return if old_mentry.format == new_mentry.format
|
|
95
|
-
|
|
96
|
-
raise UsageError.new("mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def ensure_uid!(container, call, old_mentry)
|
|
100
|
-
pre_env = reader(container).read(@old_key)
|
|
101
|
-
return if pre_env.uid
|
|
102
|
-
|
|
103
|
-
writer(container, call).put(
|
|
104
|
-
@old_key,
|
|
105
|
-
mentry: old_mentry,
|
|
106
|
-
payload: Textus::Envelope::Writer::Payload.new(
|
|
107
|
-
meta: pre_env.meta,
|
|
108
|
-
body: pre_env.body,
|
|
109
|
-
content: pre_env.content,
|
|
110
|
-
),
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def dry_run_result(container, old_res, new_res)
|
|
115
|
-
pre_env = reader(container).read(@old_key)
|
|
116
|
-
{
|
|
117
|
-
"protocol" => PROTOCOL,
|
|
118
|
-
"ok" => true,
|
|
119
|
-
"dry_run" => true,
|
|
120
|
-
"from_key" => @old_key,
|
|
121
|
-
"to_key" => @new_key,
|
|
122
|
-
"from_path" => old_res.path,
|
|
123
|
-
"to_path" => new_res.path,
|
|
124
|
-
"uid" => pre_env.uid,
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyMvPrefix < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :key_mv_prefix
|
|
9
|
-
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "key mv-prefix"
|
|
12
|
-
arg :from_prefix, String, required: true, positional: true,
|
|
13
|
-
description: "dotted prefix whose leaf keys are renamed"
|
|
14
|
-
arg :to_prefix, String, required: true, positional: true,
|
|
15
|
-
description: "dotted prefix the keys are renamed to"
|
|
16
|
-
arg :dry_run, :boolean, default: false,
|
|
17
|
-
description: "when true, returns the planned moves without applying them; defaults " \
|
|
18
|
-
"to false, so omitting it applies the rename immediately"
|
|
19
|
-
view { |v, _i| v.to_h }
|
|
20
|
-
|
|
21
|
-
def initialize(from_prefix:, to_prefix:, dry_run: false)
|
|
22
|
-
super()
|
|
23
|
-
@from_prefix = from_prefix
|
|
24
|
-
@to_prefix = to_prefix
|
|
25
|
-
@dry_run = dry_run
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def call(container:, call:)
|
|
29
|
-
raise UsageError.new("from_prefix and to_prefix required") if @from_prefix.nil? || @to_prefix.nil?
|
|
30
|
-
|
|
31
|
-
leaves = Textus::Action::List.new(prefix: @from_prefix).call(container: container)
|
|
32
|
-
.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
33
|
-
|
|
34
|
-
if leaves.include?(@from_prefix)
|
|
35
|
-
raise UsageError.new("from_prefix '#{@from_prefix}' is itself a leaf — use `mv` to rename a single key")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
warnings = []
|
|
39
|
-
warnings << "no keys under #{@from_prefix}" if leaves.empty?
|
|
40
|
-
|
|
41
|
-
steps = leaves.map do |old_key|
|
|
42
|
-
tail = old_key.delete_prefix("#{@from_prefix}.")
|
|
43
|
-
new_key = "#{@to_prefix}.#{tail}"
|
|
44
|
-
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
plan = Textus::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
48
|
-
return plan if @dry_run
|
|
49
|
-
|
|
50
|
-
steps.each do |step|
|
|
51
|
-
Textus::Action::KeyMv.new(old_key: step["from"], new_key: step["to"]).call(container: container, call: call)
|
|
52
|
-
end
|
|
53
|
-
plan
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
data/lib/textus/action/list.rb
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class List < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :list
|
|
9
|
-
summary "List keys filtered by lane and/or prefix."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
arg :prefix, String,
|
|
12
|
-
description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
|
|
13
|
-
arg :lane, String,
|
|
14
|
-
description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
|
|
15
|
-
view(:cli) { |rows| { "entries" => rows } }
|
|
16
|
-
|
|
17
|
-
def initialize(prefix: nil, lane: nil)
|
|
18
|
-
super()
|
|
19
|
-
@prefix = prefix
|
|
20
|
-
@lane = lane
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
24
|
-
manifest = container.manifest
|
|
25
|
-
rows = manifest.resolver.enumerate(prefix: @prefix)
|
|
26
|
-
rows = rows.select { |row| row[:manifest_entry].lane == @lane } if @lane
|
|
27
|
-
rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.new(*args, **kwargs)
|
|
31
|
-
return super(**kwargs) unless args.any?
|
|
32
|
-
|
|
33
|
-
call_spec = instance_method(:initialize).parameters
|
|
34
|
-
required = call_spec.slice(:keyreq).map(&:last)
|
|
35
|
-
optional = call_spec.slice(:key).map(&:last)
|
|
36
|
-
positional = required + optional
|
|
37
|
-
mapped = positional.zip(args).to_h
|
|
38
|
-
super(**mapped.merge(kwargs))
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Propose < WriteVerb
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :propose
|
|
9
|
-
summary "Write a proposal to the role's propose_lane. Auto-prefixes the key."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli_stdin :json
|
|
12
|
-
arg :key, String, required: true, positional: true,
|
|
13
|
-
description: "key relative to propose_lane, e.g. 'decisions.feature-x'"
|
|
14
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
15
|
-
description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
|
|
16
|
-
arg :body, String,
|
|
17
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
18
|
-
arg :content, Hash,
|
|
19
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
20
|
-
view { |env, _i| env.to_h_for_wire }
|
|
21
|
-
|
|
22
|
-
def initialize(key:, meta: nil, body: nil, content: nil)
|
|
23
|
-
super()
|
|
24
|
-
@key = key
|
|
25
|
-
@meta = meta
|
|
26
|
-
@body = body
|
|
27
|
-
@content = content
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def call(container:, call:)
|
|
31
|
-
zone = container.manifest.policy.propose_lane_for(call.role)
|
|
32
|
-
unless zone
|
|
33
|
-
raise Textus::Error.new(
|
|
34
|
-
"propose_forbidden",
|
|
35
|
-
"role '#{call.role}' has no writable propose_lane",
|
|
36
|
-
details: { "role" => call.role },
|
|
37
|
-
hint: "the manifest must define a queue zone and '#{call.role}' must hold the 'propose' capability",
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
run_with_cascade("#{zone}.#{@key}", container:, call:) do
|
|
42
|
-
Textus::Action::Put.new(
|
|
43
|
-
key: "#{zone}.#{@key}",
|
|
44
|
-
meta: @meta || {},
|
|
45
|
-
body: @body,
|
|
46
|
-
content: @content,
|
|
47
|
-
).call(container: container, call: call)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Published < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :published
|
|
9
|
-
summary "List all entries that declare a publish_to target."
|
|
10
|
-
surfaces :cli
|
|
11
|
-
cli "published"
|
|
12
|
-
|
|
13
|
-
def args
|
|
14
|
-
{}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def call(container:, **)
|
|
18
|
-
container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
19
|
-
{ "key" => entry.key, "publish_to" => entry.publish_to }
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/textus/action/pulse.rb
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Action
|
|
7
|
-
class Pulse < Base
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :pulse
|
|
11
|
-
summary "Delta since cursor — changed entries, pending proposals, index freshness."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
around :cursor
|
|
14
|
-
arg :since, Integer, session_default: :cursor,
|
|
15
|
-
description: "audit seq to diff from; defaults to the session cursor"
|
|
16
|
-
|
|
17
|
-
def initialize(since: nil)
|
|
18
|
-
super()
|
|
19
|
-
@since = since
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call(container:, call:)
|
|
23
|
-
@container = container
|
|
24
|
-
@call = call
|
|
25
|
-
@manifest = container.manifest
|
|
26
|
-
@audit_log = container.audit_log
|
|
27
|
-
@root = container.root
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
"cursor" => @audit_log.latest_seq,
|
|
31
|
-
"changed" => Textus::Action::Audit.new(seq_since: @since).call(container: container),
|
|
32
|
-
"pending_review" => review_keys,
|
|
33
|
-
"contract_etag" => Textus::Etag.for_contract(@root),
|
|
34
|
-
"index_etag" => index_etag(container),
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def review_keys
|
|
41
|
-
queue = @manifest.policy.queue_lane
|
|
42
|
-
return [] unless queue
|
|
43
|
-
|
|
44
|
-
rows = Textus::Action::List.new(lane: queue).call(container: @container)
|
|
45
|
-
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def index_etag(container)
|
|
49
|
-
path = container.manifest.resolver.resolve("artifacts.system.index").path
|
|
50
|
-
File.exist?(path) ? container.file_store.etag(path) : nil
|
|
51
|
-
rescue Textus::Error
|
|
52
|
-
nil
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
data/lib/textus/action/put.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Put < WriteVerb
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :put
|
|
9
|
-
summary "Create or update an entry. Schema-validated. Returns {uid, etag}."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
arg :key, String, required: true, positional: true,
|
|
12
|
-
description: "dotted entry key, e.g. 'knowledge.project'; must resolve to a zone the role may write"
|
|
13
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
14
|
-
description: "frontmatter; reads back as `_meta` from `get`. Schema-validated — call `schema KEY` first"
|
|
15
|
-
arg :body, String,
|
|
16
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
17
|
-
arg :content, Hash,
|
|
18
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
19
|
-
arg :if_etag, String,
|
|
20
|
-
description: "optimistic-concurrency guard: the etag you last read; the write is rejected if the entry changed since"
|
|
21
|
-
view { |env| { "uid" => env.uid, "etag" => env.etag } }
|
|
22
|
-
|
|
23
|
-
def initialize(key:, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
24
|
-
super()
|
|
25
|
-
@key = key
|
|
26
|
-
@meta = meta
|
|
27
|
-
@body = body
|
|
28
|
-
@content = content
|
|
29
|
-
@if_etag = if_etag
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def call(container:, call:)
|
|
33
|
-
run_with_cascade(@key, container:, call:) do
|
|
34
|
-
Textus::Manifest::Data.validate_key!(@key)
|
|
35
|
-
mentry = container.manifest.resolver.resolve(@key).entry
|
|
36
|
-
envelope = writer(container, call).put(
|
|
37
|
-
@key,
|
|
38
|
-
mentry: mentry,
|
|
39
|
-
payload: Textus::Envelope::Writer::Payload.new(
|
|
40
|
-
meta: @meta,
|
|
41
|
-
body: @body,
|
|
42
|
-
content: @content,
|
|
43
|
-
),
|
|
44
|
-
if_etag: @if_etag,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
envelope
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
data/lib/textus/action/rdeps.rb
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Rdeps < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :rdeps
|
|
9
|
-
summary "List the derived entries that depend on a key (reverse deps / impact set)."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
arg :key, String, required: true, positional: true,
|
|
12
|
-
description: "dotted key whose dependents (what would be stranded if it moved) you want"
|
|
13
|
-
|
|
14
|
-
def initialize(key:)
|
|
15
|
-
super()
|
|
16
|
-
@key = key
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(container:, **)
|
|
20
|
-
manifest = container.manifest
|
|
21
|
-
rdeps = manifest.data.entries.each_with_object([]) do |entry, acc|
|
|
22
|
-
next unless entry.external?
|
|
23
|
-
|
|
24
|
-
sources = Array(entry.source&.sources).compact
|
|
25
|
-
acc << entry.key if sources.any? { |source| source == @key || @key.start_with?("#{source}.") }
|
|
26
|
-
end
|
|
27
|
-
{ "key" => @key, "rdeps" => rdeps }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.new(*args, **kwargs)
|
|
31
|
-
return super(**kwargs) unless args.any?
|
|
32
|
-
|
|
33
|
-
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
34
|
-
mapped = positional.zip(args).to_h
|
|
35
|
-
super(**mapped.merge(kwargs))
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
data/lib/textus/action/reject.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Reject < WriteVerb
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :reject
|
|
9
|
-
summary "discard a queued proposal without applying it"
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "reject"
|
|
12
|
-
arg :pending_key, String, required: true, positional: true, description: "the queued proposal's key"
|
|
13
|
-
|
|
14
|
-
def initialize(pending_key:)
|
|
15
|
-
super()
|
|
16
|
-
@pending_key = pending_key
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(container:, call:)
|
|
20
|
-
run_with_cascade(@pending_key, container:, call:) do
|
|
21
|
-
mentry = container.manifest.resolver.resolve(@pending_key).entry
|
|
22
|
-
unless mentry.in_proposal_lane?(container.manifest.policy)
|
|
23
|
-
raise ProposalError.new("reject: '#{@pending_key}' is not in a proposal zone (zone=#{mentry.lane})")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
env = Textus::Action::Get.new(key: @pending_key).call(container: container, call: call)
|
|
27
|
-
proposal = env.meta&.dig("proposal") or raise ProposalError.new("entry has no proposal block: #{@pending_key}")
|
|
28
|
-
target_key = proposal["target_key"] or raise ProposalError.new("proposal missing target_key")
|
|
29
|
-
|
|
30
|
-
writer(container, call).delete(@pending_key, mentry: mentry)
|
|
31
|
-
|
|
32
|
-
{ "protocol" => PROTOCOL, "rejected" => @pending_key, "target_key" => target_key }
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class RuleExplain < Base
|
|
6
|
-
extend Textus::Contract::DSL
|
|
7
|
-
|
|
8
|
-
verb :rule_explain
|
|
9
|
-
summary "Effective rules for a key. Lean {lifecycle, guard} by default; detail: true adds matched blocks + guard predicates."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
cli "rule explain"
|
|
12
|
-
arg :key, String, required: true, positional: true,
|
|
13
|
-
description: "dotted key whose effective rules you want (lifecycle ttl/action, write guard, ...)"
|
|
14
|
-
arg :detail, :boolean,
|
|
15
|
-
description: "defaults false: lean {lifecycle, guard}. detail: true adds matched blocks + guard predicates per transition."
|
|
16
|
-
view(:cli) { |r| { "verb" => "rule_explain" }.merge(r.transform_keys(&:to_s)) }
|
|
17
|
-
|
|
18
|
-
def initialize(key:, detail: nil)
|
|
19
|
-
super()
|
|
20
|
-
@key = key
|
|
21
|
-
@detail = detail
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def call(container:, **)
|
|
25
|
-
@manifest = container.manifest
|
|
26
|
-
@detail ? explain(@key) : effective(@key)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
REGISTRY = Textus::Manifest::Schema::FIELD_REGISTRY
|
|
30
|
-
LEAN_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:lean) }.keys.freeze
|
|
31
|
-
DETAIL_FIELDS = REGISTRY.select { |_, m| m[:in_rule_explain].include?(:detail) }.keys.freeze
|
|
32
|
-
EFFECTIVE_FIELDS = DETAIL_FIELDS.select { |f| REGISTRY[f][:policy_class] }.freeze
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def effective(key)
|
|
37
|
-
set = @manifest.rules.for(key)
|
|
38
|
-
LEAN_FIELDS.each_with_object({}) do |field, out|
|
|
39
|
-
value = set.public_send(field)
|
|
40
|
-
out[field.to_s] = lean_value(field, value) unless value.nil?
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def lean_value(field, value)
|
|
45
|
-
case field
|
|
46
|
-
when :retention
|
|
47
|
-
retention_hash(value, string_keys: true)
|
|
48
|
-
when :react
|
|
49
|
-
value.to_h
|
|
50
|
-
else
|
|
51
|
-
value
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def explain(key)
|
|
56
|
-
matching = @manifest.rules.explain(key)
|
|
57
|
-
winners = @manifest.rules.for(key)
|
|
58
|
-
{
|
|
59
|
-
key: key,
|
|
60
|
-
matched_blocks: matching.map do |block|
|
|
61
|
-
{ match: block.match }.merge(DETAIL_FIELDS.to_h { |f| [f, !block.public_send(f).nil?] })
|
|
62
|
-
end,
|
|
63
|
-
effective: EFFECTIVE_FIELDS.to_h { |f| [f, effective_value(f, winners.public_send(f))] },
|
|
64
|
-
guards: Textus::Gate::Auth::FLOOR.keys.to_h do |action|
|
|
65
|
-
floor = Textus::Gate::Auth::FLOOR.fetch(action, [])
|
|
66
|
-
rule = Array(@manifest.rules.for(key).guard&.dig(action.to_s))
|
|
67
|
-
[action, { floor: floor, rule: rule }]
|
|
68
|
-
end,
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def effective_value(field, value)
|
|
73
|
-
return nil if value.nil?
|
|
74
|
-
|
|
75
|
-
case field
|
|
76
|
-
when :retention
|
|
77
|
-
retention_hash(value, string_keys: false)
|
|
78
|
-
when :react
|
|
79
|
-
value.to_h
|
|
80
|
-
else
|
|
81
|
-
value
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def retention_hash(retention, string_keys:)
|
|
86
|
-
h = { ttl_seconds: retention.ttl_seconds, action: retention.action }
|
|
87
|
-
string_keys ? h.transform_keys(&:to_s) : h
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Action
|
|
7
|
-
class RuleLint < Base
|
|
8
|
-
extend Textus::Contract::DSL
|
|
9
|
-
|
|
10
|
-
verb :rule_lint
|
|
11
|
-
summary "Diff candidate manifest YAML's rules against the live manifest. No writes."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
cli "rule lint"
|
|
14
|
-
arg :candidate_yaml, String, required: true, wire_name: :against, source: :file,
|
|
15
|
-
description: "path to candidate manifest YAML; its `rules:` block is diffed against the live manifest"
|
|
16
|
-
view { |v, _i| v.to_h }
|
|
17
|
-
|
|
18
|
-
def initialize(candidate_yaml:)
|
|
19
|
-
super()
|
|
20
|
-
@candidate_yaml = candidate_yaml
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def call(container:, **)
|
|
24
|
-
root = container.root
|
|
25
|
-
live_rules = current_rules(root)
|
|
26
|
-
candidate_rules = parse_candidate(@candidate_yaml)
|
|
27
|
-
|
|
28
|
-
live_by_match = live_rules.to_h { |rule| [rule["match"], rule] }
|
|
29
|
-
candidate_by_match = candidate_rules.to_h { |rule| [rule["match"], rule] }
|
|
30
|
-
|
|
31
|
-
steps = (candidate_by_match.keys - live_by_match.keys).map do |match|
|
|
32
|
-
{ "op" => "add_rule", "match" => match, "rule" => candidate_by_match[match] }
|
|
33
|
-
end
|
|
34
|
-
(live_by_match.keys - candidate_by_match.keys).each do |match|
|
|
35
|
-
steps << { "op" => "remove_rule", "match" => match }
|
|
36
|
-
end
|
|
37
|
-
(live_by_match.keys & candidate_by_match.keys).each do |match|
|
|
38
|
-
next if live_by_match[match] == candidate_by_match[match]
|
|
39
|
-
|
|
40
|
-
steps << {
|
|
41
|
-
"op" => "change_rule",
|
|
42
|
-
"match" => match,
|
|
43
|
-
"from" => live_by_match[match],
|
|
44
|
-
"to" => candidate_by_match[match],
|
|
45
|
-
}
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
Textus::Jobs::Plan.new(steps: steps, warnings: [])
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def current_rules(root)
|
|
54
|
-
raw = YAML.safe_load_file(File.join(root, "manifest.yaml"), permitted_classes: [Symbol], aliases: false)
|
|
55
|
-
Array(raw["rules"])
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def parse_candidate(yaml_text)
|
|
59
|
-
raw = YAML.safe_load(yaml_text, permitted_classes: [Symbol], aliases: false)
|
|
60
|
-
raise UsageError.new("candidate is not a YAML mapping") unless raw.is_a?(Hash)
|
|
61
|
-
|
|
62
|
-
Array(raw["rules"])
|
|
63
|
-
rescue Psych::Exception => e
|
|
64
|
-
raise UsageError.new("candidate YAML parse error: #{e.message}")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|