textus 0.55.1 → 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 +1 -1
- data/README.md +9 -9
- data/SPEC.md +14 -13
- data/docs/architecture/README.md +3 -3
- data/docs/reference/conventions.md +5 -2
- data/lib/textus/boot.rb +64 -85
- data/lib/textus/{gate → dispatch}/binder.rb +8 -10
- 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 +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +2 -2
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +1 -1
- data/lib/textus/doctor/check.rb +8 -6
- data/lib/textus/doctor.rb +1 -1
- data/lib/textus/errors.rb +2 -0
- data/lib/textus/format/base.rb +36 -8
- data/lib/textus/format/json.rb +0 -21
- data/lib/textus/format/markdown.rb +0 -21
- data/lib/textus/format/yaml.rb +0 -21
- data/lib/textus/format.rb +16 -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 -5
- data/lib/textus/manifest/capabilities.rb +1 -1
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/to_paths.rb +1 -1
- 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/orchestration.rb +55 -0
- data/lib/textus/port/audit_log.rb +6 -6
- data/lib/textus/port/build_lock.rb +1 -1
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/port/sentinel_store.rb +3 -3
- data/lib/textus/port/storage/file_store.rb +23 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +58 -2
- data/lib/textus/port/watcher_lock.rb +2 -2
- 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 +3 -10
- data/lib/textus/store/container.rb +140 -10
- data/lib/textus/store/cursor.rb +1 -1
- data/lib/textus/store/{envelope → entry}/reader.rb +8 -4
- data/lib/textus/store/{envelope → entry}/writer.rb +53 -29
- 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 +5 -3
- data/lib/textus/store/jobs/planner.rb +27 -7
- data/lib/textus/store/jobs/queue.rb +9 -1
- 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 +1 -43
- data/lib/textus/store/jobs/sweep.rb +2 -2
- data/lib/textus/store/{geometry.rb → layout.rb} +19 -3
- data/lib/textus/store.rb +53 -30
- data/lib/textus/surface/cli/runner.rb +8 -9
- data/lib/textus/surface/cli/verb/doctor.rb +3 -2
- data/lib/textus/surface/cli/verb/get.rb +5 -3
- data/lib/textus/surface/cli/verb/put.rb +5 -3
- data/lib/textus/surface/mcp/catalog.rb +26 -62
- data/lib/textus/surface/mcp/errors.rb +0 -10
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/surface/mcp/server.rb +20 -31
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +5 -4
- data/lib/textus/value/etag.rb +1 -1
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +36 -16
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +1 -1
- data/lib/textus/workflow/runner.rb +10 -18
- data/lib/textus.rb +0 -64
- metadata +70 -70
- data/lib/textus/action/accept.rb +0 -46
- data/lib/textus/action/audit.rb +0 -94
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -79
- data/lib/textus/action/boot.rb +0 -15
- data/lib/textus/action/data_mv.rb +0 -58
- data/lib/textus/action/deps.rb +0 -19
- data/lib/textus/action/doctor.rb +0 -17
- data/lib/textus/action/drain.rb +0 -31
- data/lib/textus/action/enqueue.rb +0 -37
- data/lib/textus/action/get.rb +0 -34
- data/lib/textus/action/ingest.rb +0 -199
- data/lib/textus/action/jobs.rb +0 -27
- data/lib/textus/action/key_delete.rb +0 -26
- data/lib/textus/action/key_delete_prefix.rb +0 -35
- data/lib/textus/action/key_mv.rb +0 -122
- data/lib/textus/action/key_mv_prefix.rb +0 -48
- data/lib/textus/action/list.rb +0 -28
- data/lib/textus/action/propose.rb +0 -42
- data/lib/textus/action/published.rb +0 -22
- data/lib/textus/action/pulse.rb +0 -49
- data/lib/textus/action/put.rb +0 -38
- data/lib/textus/action/rdeps.rb +0 -24
- data/lib/textus/action/reject.rb +0 -28
- data/lib/textus/action/rule_explain.rb +0 -81
- data/lib/textus/action/rule_lint.rb +0 -62
- data/lib/textus/action/rule_list.rb +0 -38
- data/lib/textus/action/schema_envelope.rb +0 -22
- data/lib/textus/action/uid.rb +0 -19
- data/lib/textus/action/where.rb +0 -21
- data/lib/textus/contract/arg.rb +0 -10
- data/lib/textus/contract/dsl.rb +0 -88
- data/lib/textus/contract/spec.rb +0 -25
- data/lib/textus/contract.rb +0 -12
- 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/format/shared.rb +0 -17
- data/lib/textus/gate/auth.rb +0 -212
- data/lib/textus/gate.rb +0 -92
- data/lib/textus/meta.rb +0 -54
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/store/compositor.rb +0 -34
- data/lib/textus/store/session.rb +0 -37
- data/lib/textus/surface/projector.rb +0 -27
- data/lib/textus/surface/role_scope.rb +0 -34
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Enqueue < Base
|
|
6
|
-
verb :enqueue
|
|
7
|
-
summary "Push a registered job type onto the convergence queue, to be run by drain/serve."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "enqueue"
|
|
10
|
-
arg :type, String, required: true, positional: true,
|
|
11
|
-
description: "registered job type (e.g. materialize, re-pull, sweep)"
|
|
12
|
-
arg :args, Hash, default: {},
|
|
13
|
-
description: "type-specific arguments (e.g. { key: ... } or { scope: ... })"
|
|
14
|
-
|
|
15
|
-
def self.call(container:, call:, type:, args: {})
|
|
16
|
-
action_class = Textus::Jobs.fetch(type.to_s)
|
|
17
|
-
|
|
18
|
-
if action_class.const_defined?(:REQUIRED_ROLE) && call.role != action_class::REQUIRED_ROLE
|
|
19
|
-
return Failure(code: :forbidden,
|
|
20
|
-
message: "role '#{call.role}' is not authorized to enqueue this job type",
|
|
21
|
-
details: { "role" => call.role, "required_role" => action_class::REQUIRED_ROLE })
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
job = Textus::Store::Jobs::Queue::Job.new(
|
|
25
|
-
type: type,
|
|
26
|
-
args: args,
|
|
27
|
-
role: call.role,
|
|
28
|
-
max_attempts: 3,
|
|
29
|
-
)
|
|
30
|
-
Textus::Store::Jobs::Queue.new(store: container.job_store).enqueue(job)
|
|
31
|
-
Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "id" => job.id })
|
|
32
|
-
rescue Textus::UsageError
|
|
33
|
-
Failure(code: :usage_error, message: "unregistered job type '#{type}'")
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
data/lib/textus/action/get.rb
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Get < Base
|
|
6
|
-
verb :get
|
|
7
|
-
summary "Read one entry - a pure on-disk read annotated with a freshness " \
|
|
8
|
-
"verdict; never ingests (quarantine freshness is drain + hook " \
|
|
9
|
-
"only, ADR 0089). Returns the envelope (uid, etag, _meta, body, " \
|
|
10
|
-
"freshness)."
|
|
11
|
-
surfaces :cli, :mcp
|
|
12
|
-
arg :key, String, required: true, positional: true,
|
|
13
|
-
description: "dotted entry key to read, e.g. 'knowledge.project'"
|
|
14
|
-
view(:default) { |v, _i| v&.to_h_for_wire }
|
|
15
|
-
|
|
16
|
-
def self.call(container:, call:, key:)
|
|
17
|
-
envelope = container.compositor.read(key)
|
|
18
|
-
return Failure(code: :not_found, message: "no entry at #{key}") unless envelope
|
|
19
|
-
|
|
20
|
-
entry = container.manifest.resolver.resolve(key).entry
|
|
21
|
-
file_stat = Textus::Port::Storage::FileStat.new
|
|
22
|
-
Success(envelope.with(freshness: freshness_evaluator(container, call, file_stat).verdict(entry)))
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.freshness_evaluator(container, call, file_stat)
|
|
26
|
-
Textus::Core::Freshness::Evaluator.new(
|
|
27
|
-
manifest: container.manifest,
|
|
28
|
-
file_stat: file_stat,
|
|
29
|
-
clock: call,
|
|
30
|
-
)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
data/lib/textus/action/ingest.rb
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "date"
|
|
5
|
-
require "digest"
|
|
6
|
-
|
|
7
|
-
module Textus
|
|
8
|
-
module Action
|
|
9
|
-
class Ingest < Base
|
|
10
|
-
verb :ingest
|
|
11
|
-
summary "Capture external source material into the raw lane. Write-once, agent-owned."
|
|
12
|
-
surfaces :cli, :mcp
|
|
13
|
-
arg :kind, String, required: true, positional: true,
|
|
14
|
-
description: "source kind: url | file | asset"
|
|
15
|
-
arg :slug, String, required: true,
|
|
16
|
-
description: "human slug for the key suffix (kebab-case)"
|
|
17
|
-
arg :url, String, description: "remote URL (required when kind=url)"
|
|
18
|
-
arg :path, String, description: "local file path (required when kind=file or kind=asset)"
|
|
19
|
-
arg :zone, String, description: "asset group subdirectory (required when kind=asset)"
|
|
20
|
-
arg :label, String, description: "human label stored in source.label"
|
|
21
|
-
view { |env| { "key" => env.key, "uid" => env.uid, "etag" => env.etag } }
|
|
22
|
-
|
|
23
|
-
SOURCE_KINDS = %w[url file asset].freeze
|
|
24
|
-
CONTENT_HASH_ALGO = "sha256"
|
|
25
|
-
TOMBSTONE_RETAIN = %w[ingested_at].freeze
|
|
26
|
-
|
|
27
|
-
def self.call(container:, call:, kind:, slug:, url: nil, path: nil, zone: nil, label: nil, **) # rubocop:disable Metrics/ParameterLists
|
|
28
|
-
validation = validate_inputs(kind:, url:, path:, zone:)
|
|
29
|
-
return validation if validation.is_a?(Dry::Monads::Result::Failure)
|
|
30
|
-
|
|
31
|
-
now = Time.now.utc
|
|
32
|
-
key = derive_key(now, kind:, slug:)
|
|
33
|
-
|
|
34
|
-
content_hash = compute_content_hash(kind:, url:, path:)
|
|
35
|
-
writer = Textus::Store::Envelope::Writer.from(container: container, call: call)
|
|
36
|
-
mentry = container.manifest.resolver.resolve(key).entry
|
|
37
|
-
ts = now.iso8601
|
|
38
|
-
structured = build_structured(ts, container, now, content_hash, kind:, url:, path:, label:, zone:)
|
|
39
|
-
|
|
40
|
-
store = container.job_store
|
|
41
|
-
index = Textus::Store::Index::Lookup.new(store: store)
|
|
42
|
-
duplicate_key = find_duplicate(index, content_hash, kind:, url:)
|
|
43
|
-
|
|
44
|
-
result = if duplicate_key && duplicate_key != key
|
|
45
|
-
supersede_entry(duplicate_key, key, structured, container, call, store: store, kind:, zone:)
|
|
46
|
-
else
|
|
47
|
-
env = write_raw_entry(key, structured, mentry, writer)
|
|
48
|
-
rebuild_index(container, store)
|
|
49
|
-
env
|
|
50
|
-
end
|
|
51
|
-
Success(result)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.validate_inputs(kind:, url:, path:, zone:)
|
|
55
|
-
unless SOURCE_KINDS.include?(kind)
|
|
56
|
-
return Failure(code: :usage_error,
|
|
57
|
-
message: "ingest kind must be one of #{SOURCE_KINDS.join("|")}, got #{kind.inspect}")
|
|
58
|
-
end
|
|
59
|
-
case kind
|
|
60
|
-
when "url"
|
|
61
|
-
return Failure(code: :usage_error, message: "ingest url requires --url") unless url
|
|
62
|
-
when "file"
|
|
63
|
-
return Failure(code: :usage_error, message: "ingest file requires --path") unless path
|
|
64
|
-
when "asset"
|
|
65
|
-
return Failure(code: :usage_error, message: "ingest asset requires --path") unless path
|
|
66
|
-
return Failure(code: :usage_error, message: "ingest asset requires --zone") unless zone
|
|
67
|
-
end
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Key derivation for Gate pre-dispatch auth. Must match the runtime
|
|
72
|
-
# derivation in #call so the same key is checked by auth and used by
|
|
73
|
-
# the action body.
|
|
74
|
-
def self.dispatch_key(kind:, slug:, **)
|
|
75
|
-
derive_key(Time.now.utc, kind:, slug:)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def self.derive_key(now, kind:, slug:)
|
|
79
|
-
date = now.strftime("%Y.%m.%d")
|
|
80
|
-
"raw.#{date}.#{kind}-#{slug}"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.compute_content_hash(kind:, url:, path:)
|
|
84
|
-
digest = Digest::SHA256.new
|
|
85
|
-
case kind
|
|
86
|
-
when "url"
|
|
87
|
-
digest.update(url)
|
|
88
|
-
when "file", "asset"
|
|
89
|
-
digest.file(path)
|
|
90
|
-
end
|
|
91
|
-
"#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.build_structured(timestamp, container, now, content_hash, kind:, url:, path:, label:, zone:) # rubocop:disable Metrics/ParameterLists
|
|
95
|
-
base = { "ingested_at" => timestamp, "content_hash" => content_hash }
|
|
96
|
-
case kind
|
|
97
|
-
when "url"
|
|
98
|
-
base.merge("source" => { "kind" => "url", "url" => url, "label" => label || url },
|
|
99
|
-
"body" => nil)
|
|
100
|
-
when "file"
|
|
101
|
-
body_content = File.read(path)
|
|
102
|
-
base.merge("source" => { "kind" => "file", "path" => path,
|
|
103
|
-
"label" => label || File.basename(path) },
|
|
104
|
-
"body" => body_content)
|
|
105
|
-
when "asset"
|
|
106
|
-
asset_rel = copy_asset_file(container, now, path:, zone:)
|
|
107
|
-
base.merge("source" => { "kind" => "asset",
|
|
108
|
-
"label" => label || File.basename(path) },
|
|
109
|
-
"asset" => asset_rel,
|
|
110
|
-
"body" => nil)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def self.write_raw_entry(key, structured, mentry, writer)
|
|
115
|
-
writer.put(key, mentry: mentry,
|
|
116
|
-
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
117
|
-
meta: nil, body: nil, content: structured,
|
|
118
|
-
))
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def self.find_duplicate(index, content_hash, kind:, url:)
|
|
122
|
-
dup = index.find_by_hash(content_hash)
|
|
123
|
-
return dup if dup
|
|
124
|
-
|
|
125
|
-
return unless kind == "url"
|
|
126
|
-
|
|
127
|
-
index.find_by_url(url)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def self.rebuild_index(container, store)
|
|
131
|
-
Textus::Store::Index::Builder.new(store: store).rebuild!(resolver: container.manifest.resolver)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def self.supersede_entry(old_key, new_key, structured, container, call, store:, kind:, zone:) # rubocop:disable Metrics/ParameterLists
|
|
135
|
-
old_mentry = container.manifest.resolver.resolve(old_key).entry
|
|
136
|
-
writer = Textus::Store::Envelope::Writer.from(container: container, call: call)
|
|
137
|
-
|
|
138
|
-
reader = Textus::Store::Envelope::Reader.from(container: container)
|
|
139
|
-
old_env = reader.read(old_key)
|
|
140
|
-
old_content = old_env&.content || {}
|
|
141
|
-
tombstone = {}
|
|
142
|
-
TOMBSTONE_RETAIN.each do |k|
|
|
143
|
-
tombstone[k] = old_content[k] if old_content.key?(k)
|
|
144
|
-
end
|
|
145
|
-
source_kind = old_content.dig("source", "kind")
|
|
146
|
-
tombstone["source"] = { "kind" => source_kind } if source_kind
|
|
147
|
-
tombstone["superseded_by"] = new_key
|
|
148
|
-
|
|
149
|
-
writer.put(old_key, mentry: old_mentry,
|
|
150
|
-
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
151
|
-
meta: nil, body: nil, content: tombstone,
|
|
152
|
-
))
|
|
153
|
-
|
|
154
|
-
structured["supersedes"] = old_key
|
|
155
|
-
env = write_raw_entry(new_key, structured, container.manifest.resolver.resolve(new_key).entry, writer)
|
|
156
|
-
|
|
157
|
-
move_asset_file(container, old_content["asset"], zone:) if kind == "asset" && old_content["asset"]
|
|
158
|
-
|
|
159
|
-
rebuild_index(container, store)
|
|
160
|
-
env
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def self.move_asset_file(container, old_asset_rel, zone:)
|
|
164
|
-
old_path = File.join(container.root, "assets", old_asset_rel)
|
|
165
|
-
return unless File.exist?(old_path)
|
|
166
|
-
|
|
167
|
-
now = Time.now.utc
|
|
168
|
-
date_path = now.strftime("%Y/%m/%d")
|
|
169
|
-
filename = File.basename(old_path)
|
|
170
|
-
new_dir = File.join(container.root, "assets", "raw", date_path, zone)
|
|
171
|
-
new_path = File.join(new_dir, filename)
|
|
172
|
-
|
|
173
|
-
return if old_path == new_path
|
|
174
|
-
|
|
175
|
-
FileUtils.mkdir_p(new_dir)
|
|
176
|
-
FileUtils.mv(old_path, new_path)
|
|
177
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
|
178
|
-
warn "[textus ingest] could not move asset #{old_asset_rel}: #{e.message}"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def self.copy_asset_file(container, now, path:, zone:)
|
|
182
|
-
date_path = now.strftime("%Y/%m/%d")
|
|
183
|
-
filename = File.basename(path)
|
|
184
|
-
assets_dir = File.join(container.root, "assets", "raw", date_path, zone)
|
|
185
|
-
FileUtils.mkdir_p(assets_dir)
|
|
186
|
-
FileUtils.cp(path, File.join(assets_dir, filename))
|
|
187
|
-
create_gitignore_sentinel(container)
|
|
188
|
-
"raw/#{date_path}/#{zone}/#{filename}"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def self.create_gitignore_sentinel(container)
|
|
192
|
-
assets_root = File.join(container.root, "assets")
|
|
193
|
-
FileUtils.mkdir_p(assets_root)
|
|
194
|
-
sentinel = File.join(assets_root, ".gitignore")
|
|
195
|
-
File.write(sentinel, "*\n") unless File.exist?(sentinel)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
data/lib/textus/action/jobs.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Jobs < Base
|
|
6
|
-
verb :jobs
|
|
7
|
-
summary "List queued jobs by state; retry a dead-lettered job or purge a state."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "jobs"
|
|
10
|
-
arg :state, String, default: "ready", description: "ready|leased|done|failed"
|
|
11
|
-
arg :action, String, default: nil, description: "retry|purge (optional)"
|
|
12
|
-
arg :job_id, String, default: nil, description: "job id (required for action=retry)"
|
|
13
|
-
|
|
14
|
-
def self.call(container:, call:, state: "ready", action: nil, job_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
-
queue = Textus::Store::Jobs::Queue.new(store: container.job_store)
|
|
16
|
-
case action
|
|
17
|
-
when "retry"
|
|
18
|
-
queue.retry_failed(job_id)
|
|
19
|
-
when "purge"
|
|
20
|
-
queue.purge(state)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "state" => state, "jobs" => queue.list(state) })
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyDelete < Base
|
|
6
|
-
verb :key_delete
|
|
7
|
-
summary "Delete one entry by key. Single-key, lower blast radius than key_delete_prefix; " \
|
|
8
|
-
"guarded by an optional optimistic-concurrency etag. Returns {ok, key, deleted}."
|
|
9
|
-
surfaces :cli, :mcp
|
|
10
|
-
cli "key delete"
|
|
11
|
-
arg :key, String, required: true, positional: true,
|
|
12
|
-
description: "dotted entry key to delete"
|
|
13
|
-
arg :if_etag, String,
|
|
14
|
-
description: "optimistic-concurrency guard: the etag you last read; the delete is rejected if the entry changed since"
|
|
15
|
-
|
|
16
|
-
def self.call(container:, call:, key:, if_etag: nil)
|
|
17
|
-
Textus::Manifest::Data.validate_key!(key)
|
|
18
|
-
mentry = container.manifest.resolver.resolve(key).entry
|
|
19
|
-
|
|
20
|
-
container.compositor.delete(key, mentry: mentry, if_etag: if_etag, call: call)
|
|
21
|
-
|
|
22
|
-
Success({ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true })
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyDeletePrefix < Base
|
|
6
|
-
verb :key_delete_prefix
|
|
7
|
-
summary "Bulk-delete every leaf key under prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "key delete-prefix"
|
|
10
|
-
arg :prefix, String, required: true, positional: true,
|
|
11
|
-
description: "every leaf key under this dotted prefix is deleted"
|
|
12
|
-
arg :dry_run, :boolean, default: false,
|
|
13
|
-
description: "when true, returns the keys that would be deleted without deleting them; " \
|
|
14
|
-
"defaults to false, so omitting it deletes immediately"
|
|
15
|
-
view { |v, _i| v.to_h }
|
|
16
|
-
|
|
17
|
-
def self.call(container:, call:, prefix:, dry_run: false)
|
|
18
|
-
return Failure(code: :usage_error, message: "prefix required") if prefix.nil? || prefix.empty?
|
|
19
|
-
|
|
20
|
-
leaves = Textus::Action::List.leaf_keys(container: container, prefix: prefix)
|
|
21
|
-
|
|
22
|
-
warnings = leaves.empty? ? ["no keys under #{prefix}"] : []
|
|
23
|
-
steps = leaves.map { |key| { "op" => "delete", "key" => key } }
|
|
24
|
-
|
|
25
|
-
plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
26
|
-
return Success(plan) if dry_run
|
|
27
|
-
|
|
28
|
-
steps.each do |step|
|
|
29
|
-
Value::Result.unwrap(Textus::Action::KeyDelete.call(container: container, call: call, key: step["key"]))
|
|
30
|
-
end
|
|
31
|
-
Success(plan)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
data/lib/textus/action/key_mv.rb
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyMv < Base
|
|
6
|
-
verb :key_mv
|
|
7
|
-
summary "Rename one entry (same zone + format). Refuses if the target exists. Single-key, lower blast radius than key_mv_prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "key mv"
|
|
10
|
-
arg :old_key, String, required: true, positional: true,
|
|
11
|
-
description: "current dotted key"
|
|
12
|
-
arg :new_key, String, required: true, positional: true,
|
|
13
|
-
description: "new dotted key (must be the same zone and format as old_key)"
|
|
14
|
-
arg :dry_run, :boolean,
|
|
15
|
-
description: "when true, returns the planned move (from/to paths, uid) without applying it; " \
|
|
16
|
-
"defaults to false, so omitting it applies the move immediately " \
|
|
17
|
-
"(unlike the bulk key_mv_prefix, which defaults to a dry-run plan)"
|
|
18
|
-
|
|
19
|
-
def self.call(container:, call:, old_key:, new_key:, dry_run: false)
|
|
20
|
-
execute_move(container: container, call: call, old_key: old_key, new_key: new_key, dry_run: dry_run)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.execute_move(container:, call:, old_key:, new_key:, dry_run:)
|
|
24
|
-
prepared = prepare(container: container, old_key: old_key, new_key: new_key)
|
|
25
|
-
return prepared if prepared.is_a?(Dry::Monads::Result::Failure)
|
|
26
|
-
|
|
27
|
-
old_res, new_res = prepared
|
|
28
|
-
if dry_run
|
|
29
|
-
return Success(dry_run_result(container: container, old_key: old_key, new_key: new_key, old_res: old_res,
|
|
30
|
-
new_res: new_res))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
envelope = apply_move(container: container, call: call, old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res)
|
|
34
|
-
Success(success_result(old_key: old_key, new_key: new_key, old_res: old_res, new_res: new_res, envelope: envelope))
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.apply_move(container:, call:, old_key:, new_key:, old_res:, new_res:)
|
|
38
|
-
ensure_uid!(container: container, call: call, old_key: old_key, old_mentry: old_res.entry)
|
|
39
|
-
container.compositor.move(
|
|
40
|
-
from_key: old_key,
|
|
41
|
-
to_key: new_key,
|
|
42
|
-
new_mentry: new_res.entry,
|
|
43
|
-
call: call,
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def self.success_result(old_key:, new_key:, old_res:, new_res:, envelope:)
|
|
48
|
-
{
|
|
49
|
-
"protocol" => Textus::PROTOCOL,
|
|
50
|
-
"ok" => true,
|
|
51
|
-
"from_key" => old_key,
|
|
52
|
-
"to_key" => new_key,
|
|
53
|
-
"from_path" => old_res.path,
|
|
54
|
-
"to_path" => new_res.path,
|
|
55
|
-
"uid" => envelope.uid,
|
|
56
|
-
"envelope" => envelope.to_h_for_wire,
|
|
57
|
-
}
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def self.prepare(container:, old_key:, new_key:)
|
|
61
|
-
Textus::Manifest::Data.validate_key!(old_key)
|
|
62
|
-
Textus::Manifest::Data.validate_key!(new_key)
|
|
63
|
-
return Failure(code: :usage_error, message: "mv: old and new keys are identical") if old_key == new_key
|
|
64
|
-
|
|
65
|
-
old_res = container.manifest.resolver.resolve(old_key)
|
|
66
|
-
new_res = container.manifest.resolver.resolve(new_key)
|
|
67
|
-
return Failure(code: :not_found, message: "source key '#{old_key}' not found") unless container.compositor.exists?(old_key)
|
|
68
|
-
|
|
69
|
-
zone_check = validate_zone_and_format(old_mentry: old_res.entry, new_mentry: new_res.entry)
|
|
70
|
-
return zone_check if zone_check.is_a?(Dry::Monads::Result::Failure)
|
|
71
|
-
|
|
72
|
-
if container.compositor.exists?(new_key)
|
|
73
|
-
return Failure(code: :usage_error, message: "mv: target '#{new_key}' already exists at #{new_res.path}")
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
[old_res, new_res]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def self.validate_zone_and_format(old_mentry:, new_mentry:)
|
|
80
|
-
if old_mentry.lane != new_mentry.lane
|
|
81
|
-
return Failure(code: :usage_error,
|
|
82
|
-
message: "mv: cross-zone move refused (#{old_mentry.lane} -> #{new_mentry.lane}). " \
|
|
83
|
-
"Use put+delete for cross-zone moves.")
|
|
84
|
-
end
|
|
85
|
-
return unless old_mentry.format != new_mentry.format
|
|
86
|
-
|
|
87
|
-
Failure(code: :usage_error,
|
|
88
|
-
message: "mv: format mismatch (#{old_mentry.format} -> #{new_mentry.format}); refusing.")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def self.ensure_uid!(container:, call:, old_key:, old_mentry:)
|
|
92
|
-
pre_env = container.compositor.read(old_key)
|
|
93
|
-
return if pre_env.uid
|
|
94
|
-
|
|
95
|
-
container.compositor.write(
|
|
96
|
-
old_key,
|
|
97
|
-
mentry: old_mentry,
|
|
98
|
-
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
99
|
-
meta: pre_env.meta,
|
|
100
|
-
body: pre_env.body,
|
|
101
|
-
content: pre_env.content,
|
|
102
|
-
),
|
|
103
|
-
call: call,
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.dry_run_result(container:, old_key:, new_key:, old_res:, new_res:)
|
|
108
|
-
pre_env = container.compositor.read(old_key)
|
|
109
|
-
{
|
|
110
|
-
"protocol" => Textus::PROTOCOL,
|
|
111
|
-
"ok" => true,
|
|
112
|
-
"dry_run" => true,
|
|
113
|
-
"from_key" => old_key,
|
|
114
|
-
"to_key" => new_key,
|
|
115
|
-
"from_path" => old_res.path,
|
|
116
|
-
"to_path" => new_res.path,
|
|
117
|
-
"uid" => pre_env.uid,
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class KeyMvPrefix < Base
|
|
6
|
-
verb :key_mv_prefix
|
|
7
|
-
summary "Bulk-rename every leaf key under from_prefix to to_prefix. Dry-run returns a Plan; apply with dry_run: false."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli "key mv-prefix"
|
|
10
|
-
arg :from_prefix, String, required: true, positional: true,
|
|
11
|
-
description: "dotted prefix whose leaf keys are renamed"
|
|
12
|
-
arg :to_prefix, String, required: true, positional: true,
|
|
13
|
-
description: "dotted prefix the keys are renamed to"
|
|
14
|
-
arg :dry_run, :boolean, default: false,
|
|
15
|
-
description: "when true, returns the planned moves without applying them; defaults " \
|
|
16
|
-
"to false, so omitting it applies the rename immediately"
|
|
17
|
-
view { |v, _i| v.to_h }
|
|
18
|
-
|
|
19
|
-
def self.call(container:, call:, from_prefix:, to_prefix:, dry_run: false)
|
|
20
|
-
return Failure(code: :usage_error, message: "from_prefix and to_prefix required") if from_prefix.nil? || to_prefix.nil?
|
|
21
|
-
|
|
22
|
-
leaves = Textus::Action::List.leaf_keys(container: container, prefix: from_prefix)
|
|
23
|
-
|
|
24
|
-
if leaves.include?(from_prefix)
|
|
25
|
-
return Failure(code: :usage_error,
|
|
26
|
-
message: "from_prefix '#{from_prefix}' is itself a leaf — use `mv` to rename a single key")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
warnings = []
|
|
30
|
-
warnings << "no keys under #{from_prefix}" if leaves.empty?
|
|
31
|
-
|
|
32
|
-
steps = leaves.map do |old_key|
|
|
33
|
-
tail = old_key.delete_prefix("#{from_prefix}.")
|
|
34
|
-
new_key = "#{to_prefix}.#{tail}"
|
|
35
|
-
{ "op" => "mv", "from" => old_key, "to" => new_key }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
plan = Textus::Store::Jobs::Plan.new(steps: steps, warnings: warnings)
|
|
39
|
-
return Success(plan) if dry_run
|
|
40
|
-
|
|
41
|
-
steps.each do |step|
|
|
42
|
-
Value::Result.unwrap(Textus::Action::KeyMv.call(container: container, call: call, old_key: step["from"], new_key: step["to"]))
|
|
43
|
-
end
|
|
44
|
-
Success(plan)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
data/lib/textus/action/list.rb
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class List < Base
|
|
6
|
-
verb :list
|
|
7
|
-
summary "List keys filtered by lane and/or prefix."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
arg :prefix, String,
|
|
10
|
-
description: "restrict to keys starting with this dotted prefix, e.g. 'knowledge.runbooks'"
|
|
11
|
-
arg :lane, String,
|
|
12
|
-
description: "restrict to one lane by name (see `boot` lanes); combine with prefix to narrow further"
|
|
13
|
-
view(:cli) { |rows| { "entries" => rows } }
|
|
14
|
-
|
|
15
|
-
def self.call(container:, call: nil, prefix: nil, lane: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
16
|
-
manifest = container.manifest
|
|
17
|
-
rows = manifest.resolver.enumerate(prefix: prefix)
|
|
18
|
-
rows = rows.select { |row| row[:manifest_entry].lane == lane } if lane
|
|
19
|
-
Success(rows.map { |row| { "key" => row[:key], "lane" => row[:manifest_entry].lane, "path" => row[:path] } })
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.leaf_keys(container:, prefix: nil, lane: nil)
|
|
23
|
-
rows = Value::Result.unwrap(call(container: container, prefix: prefix, lane: lane))
|
|
24
|
-
rows.map { |row| row.is_a?(Hash) ? (row["key"] || row[:key]) : row }
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Propose < Base
|
|
6
|
-
verb :propose
|
|
7
|
-
summary "Write a proposal to the role's propose_lane. Auto-prefixes the key."
|
|
8
|
-
surfaces :cli, :mcp
|
|
9
|
-
cli_stdin :json
|
|
10
|
-
arg :key, String, required: true, positional: true,
|
|
11
|
-
description: "key relative to propose_lane, e.g. 'decisions.feature-x'"
|
|
12
|
-
arg :meta, Hash, required: false, wire_name: :_meta,
|
|
13
|
-
description: "frontmatter; reads back as `_meta` from `get`. Include a 'proposal:' block naming the target_key"
|
|
14
|
-
arg :body, String,
|
|
15
|
-
description: "markdown/text payload for markdown-format entries; omit (use `content`) for json/yaml entries. Do not send both"
|
|
16
|
-
arg :content, Hash,
|
|
17
|
-
description: "structured payload for json/yaml-format entries; omit (use `body`) for markdown entries. Do not send both"
|
|
18
|
-
view { |env, _i| env.to_h_for_wire }
|
|
19
|
-
|
|
20
|
-
def self.call(container:, call:, key:, meta: nil, body: nil, content: nil)
|
|
21
|
-
zone = container.manifest.policy.propose_lane_for(call.role)
|
|
22
|
-
unless zone
|
|
23
|
-
return Failure(code: :propose_forbidden,
|
|
24
|
-
message: "role '#{call.role}' has no writable propose_lane",
|
|
25
|
-
details: { "role" => call.role })
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
mentry = container.manifest.resolver.resolve("#{zone}.#{key}").entry
|
|
29
|
-
Success(container.compositor.write(
|
|
30
|
-
"#{zone}.#{key}",
|
|
31
|
-
mentry: mentry,
|
|
32
|
-
payload: Textus::Store::Envelope::Writer::Payload.new(
|
|
33
|
-
meta: meta || {},
|
|
34
|
-
body: body,
|
|
35
|
-
content: content,
|
|
36
|
-
),
|
|
37
|
-
call: call,
|
|
38
|
-
))
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Action
|
|
5
|
-
class Published < Base
|
|
6
|
-
verb :published
|
|
7
|
-
summary "List all entries that declare a publish_to target."
|
|
8
|
-
surfaces :cli
|
|
9
|
-
cli "published"
|
|
10
|
-
|
|
11
|
-
def args
|
|
12
|
-
{}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.call(container:, **)
|
|
16
|
-
Success(container.manifest.data.entries.reject { |entry| entry.publish_to.empty? }.map do |entry|
|
|
17
|
-
{ "key" => entry.key, "publish_to" => entry.publish_to }
|
|
18
|
-
end)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
data/lib/textus/action/pulse.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
|
|
5
|
-
module Textus
|
|
6
|
-
module Action
|
|
7
|
-
class Pulse < Base
|
|
8
|
-
verb :pulse
|
|
9
|
-
summary "Delta since cursor — changed entries, pending proposals, index freshness."
|
|
10
|
-
surfaces :cli, :mcp
|
|
11
|
-
arg :since, Integer, session_default: :cursor,
|
|
12
|
-
description: "audit seq to diff from; defaults to the session cursor"
|
|
13
|
-
|
|
14
|
-
def self.call(container:, call:, since: nil, **)
|
|
15
|
-
manifest = container.manifest
|
|
16
|
-
audit_log = container.audit_log
|
|
17
|
-
root = container.root
|
|
18
|
-
since ||= Textus::Store::Cursor.new(root: root, role: call.role).read
|
|
19
|
-
|
|
20
|
-
changed = Value::Result.unwrap(Textus::Action::Audit.call(container: container, seq_since: since))
|
|
21
|
-
|
|
22
|
-
result = {
|
|
23
|
-
"cursor" => audit_log.latest_seq,
|
|
24
|
-
"changed" => changed,
|
|
25
|
-
"pending_review" => review_keys(manifest, container),
|
|
26
|
-
"contract_etag" => Textus::Value::Etag.for_contract(root),
|
|
27
|
-
"index_etag" => index_etag(container),
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
Textus::Store::Cursor.new(root: root, role: call.role).write(result["cursor"])
|
|
31
|
-
Success(result)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.review_keys(manifest, container)
|
|
35
|
-
queue = manifest.policy.queue_lane
|
|
36
|
-
return [] unless queue
|
|
37
|
-
|
|
38
|
-
Textus::Action::List.leaf_keys(container: container, lane: queue)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.index_etag(container)
|
|
42
|
-
path = container.manifest.resolver.resolve("artifacts.system.index").path
|
|
43
|
-
File.exist?(path) ? container.file_store.etag(path) : nil
|
|
44
|
-
rescue Textus::Error
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|