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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
|
-
class
|
|
4
|
+
class ScratchpadSources < Check
|
|
5
5
|
def call
|
|
6
6
|
issues = []
|
|
7
7
|
manifest.resolver.enumerate.each do |row|
|
|
8
|
-
next unless row[:key].start_with?("
|
|
8
|
+
next unless row[:key].start_with?("scratchpad.notes.")
|
|
9
9
|
next unless row[:path] && File.exist?(row[:path])
|
|
10
10
|
|
|
11
11
|
sources = parse_sources(row[:path])
|
|
@@ -13,10 +13,10 @@ module Textus
|
|
|
13
13
|
next if raw_entry_exists?(raw_key)
|
|
14
14
|
|
|
15
15
|
issues << {
|
|
16
|
-
"code" => "
|
|
16
|
+
"code" => "scratchpad.source_missing",
|
|
17
17
|
"level" => "warning",
|
|
18
18
|
"subject" => row[:key],
|
|
19
|
-
"message" => "
|
|
19
|
+
"message" => "scratchpad entry '#{row[:key]}' references raw key '#{raw_key}' " \
|
|
20
20
|
"which does not exist in the store",
|
|
21
21
|
"fix" => "re-ingest the source: textus ingest ..., or remove the stale sources: entry",
|
|
22
22
|
}
|
|
@@ -33,7 +33,12 @@ module Textus
|
|
|
33
33
|
return [] unless match
|
|
34
34
|
|
|
35
35
|
front = YAML.safe_load(match[1])
|
|
36
|
-
Array(front&.dig("sources"))
|
|
36
|
+
Array(front&.dig("sources")).filter_map do |s|
|
|
37
|
+
case s
|
|
38
|
+
when String then s
|
|
39
|
+
when Hash then s["key"]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
37
42
|
rescue StandardError
|
|
38
43
|
[]
|
|
39
44
|
end
|
|
@@ -3,13 +3,13 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class Sentinels < Check
|
|
5
5
|
def call
|
|
6
|
-
store = Textus::
|
|
7
|
-
file_stat = Textus::
|
|
8
|
-
dir = Textus::Layout.
|
|
6
|
+
store = Textus::Port::SentinelStore.new
|
|
7
|
+
file_stat = Textus::Port::Storage::FileStat.new
|
|
8
|
+
dir = Textus::Store::Layout.new(root).sentinels_root
|
|
9
9
|
return [] unless file_stat.directory?(dir)
|
|
10
10
|
|
|
11
11
|
repo_root = File.dirname(root)
|
|
12
|
-
file_stat.glob(File.join(dir, "**", "*#{Textus::
|
|
12
|
+
file_stat.glob(File.join(dir, "**", "*#{Textus::Port::SentinelStore::SUFFIX}")).flat_map do |sentinel_path|
|
|
13
13
|
inspect_sentinel(sentinel_path, repo_root, store, file_stat)
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/textus/doctor/check.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Textus
|
|
|
14
14
|
.downcase
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def initialize(container, role: Textus::Role::DEFAULT)
|
|
17
|
+
def initialize(container, role: Textus::Value::Role::DEFAULT)
|
|
18
18
|
@container = container
|
|
19
19
|
@role = role
|
|
20
20
|
end
|
|
@@ -26,18 +26,17 @@ module Textus
|
|
|
26
26
|
protected
|
|
27
27
|
|
|
28
28
|
def root = @container.root
|
|
29
|
+
def geometry = @container.layout
|
|
29
30
|
def manifest = @container.manifest
|
|
30
31
|
|
|
31
|
-
# Dispatch a verb through Gate.
|
|
32
32
|
def dispatch(verb, *args, **kwargs)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@container.gate.dispatch(cmd)
|
|
33
|
+
contract_class = Textus::VerbRegistry.contract_class_for(verb)
|
|
34
|
+
members = contract_class.members
|
|
35
|
+
command_kwargs = members.each_with_index.to_h { |m, i| [m, args[i] || kwargs[m]] }
|
|
36
|
+
command = contract_class.new(**command_kwargs)
|
|
37
|
+
call = Textus::Value::Call.build(role: @role)
|
|
38
|
+
result = @container.pipeline.dispatch(command, call: call)
|
|
39
|
+
Textus::Value::Result.extract(result)
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
end
|
data/lib/textus/doctor.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Textus
|
|
|
22
22
|
Check::ProposalTargets,
|
|
23
23
|
Check::GeneratorDrift,
|
|
24
24
|
Check::RawAssetPaths,
|
|
25
|
-
Check::
|
|
25
|
+
Check::ScratchpadSources,
|
|
26
26
|
Check::StaleReviewedStamp,
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ module Textus
|
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
-
def build(container:, checks: nil, role: Textus::Role::DEFAULT)
|
|
33
|
+
def build(container:, checks: nil, role: Textus::Value::Role::DEFAULT)
|
|
34
34
|
selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
|
|
35
35
|
unknown = selected_keys - ALL_CHECKS
|
|
36
36
|
unless unknown.empty?
|
data/lib/textus/errors.rb
CHANGED
|
@@ -225,7 +225,15 @@ module Textus
|
|
|
225
225
|
end
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
+
class ActionError < Error
|
|
229
|
+
def initialize(code, message, details: {})
|
|
230
|
+
super(code, message, details: details, exit_code: 1)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
228
234
|
class CursorExpired < Error
|
|
235
|
+
JSONRPC_CODE = -32_002
|
|
236
|
+
|
|
229
237
|
attr_reader :requested, :min_available
|
|
230
238
|
|
|
231
239
|
def initialize(requested:, min_available:)
|
data/lib/textus/format/base.rb
CHANGED
|
@@ -25,18 +25,46 @@ module Textus
|
|
|
25
25
|
raise NotImplementedError.new("#{name}.nested_glob not implemented")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def self.validate_path_extension(
|
|
29
|
-
|
|
28
|
+
def self.validate_path_extension(path, nested)
|
|
29
|
+
ext = File.extname(path)
|
|
30
|
+
if nested
|
|
31
|
+
return if ext == ""
|
|
32
|
+
|
|
33
|
+
raise UsageError.new("#{format_name} nested path must not have an extension")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return if extensions.include?(ext)
|
|
37
|
+
|
|
38
|
+
raise UsageError.new("#{format_name} format requires '#{extensions.join("' or '")}' path (got #{ext.inspect})")
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
def self.
|
|
33
|
-
|
|
41
|
+
def self.enforce_name_match!(path, meta)
|
|
42
|
+
return unless meta.is_a?(Hash) && meta["name"]
|
|
43
|
+
|
|
44
|
+
ext = extensions.first
|
|
45
|
+
basename = File.basename(path, ext)
|
|
46
|
+
return if meta["name"] == basename
|
|
47
|
+
|
|
48
|
+
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
34
49
|
end
|
|
35
50
|
|
|
36
|
-
def self.
|
|
37
|
-
|
|
51
|
+
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
52
|
+
raw = File.binread(path)
|
|
53
|
+
parsed = parse(raw, path: path)
|
|
54
|
+
meta = parsed["_meta"] || {}
|
|
55
|
+
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
56
|
+
|
|
57
|
+
new_meta = meta.merge("name" => basename)
|
|
58
|
+
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"] || "", content: parsed["content"]))
|
|
59
|
+
true
|
|
38
60
|
end
|
|
39
61
|
|
|
62
|
+
def self.format_name
|
|
63
|
+
name.split("::").last.downcase
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.validate_raw_entry!(_parsed, _lane); end
|
|
67
|
+
|
|
40
68
|
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
41
69
|
_ = meta
|
|
42
70
|
_ = body
|
|
@@ -44,10 +72,6 @@ module Textus
|
|
|
44
72
|
_ = path
|
|
45
73
|
raise NotImplementedError.new("#{name}.serialize_for_put not implemented")
|
|
46
74
|
end
|
|
47
|
-
|
|
48
|
-
def self.rewrite_name(_path, _basename)
|
|
49
|
-
raise NotImplementedError.new("#{name}.rewrite_name not implemented")
|
|
50
|
-
end
|
|
51
75
|
end
|
|
52
76
|
end
|
|
53
77
|
end
|
data/lib/textus/format/json.rb
CHANGED
|
@@ -59,33 +59,6 @@ module Textus
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
63
|
-
raw = File.binread(path)
|
|
64
|
-
parsed = parse(raw, path: path)
|
|
65
|
-
meta = parsed["_meta"]
|
|
66
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
67
|
-
|
|
68
|
-
new_meta = meta.merge("name" => basename)
|
|
69
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
70
|
-
true
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def self.enforce_name_match!(path, meta)
|
|
74
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
75
|
-
|
|
76
|
-
ext = extensions.first
|
|
77
|
-
basename = File.basename(path, ext)
|
|
78
|
-
return if meta["name"] == basename
|
|
79
|
-
|
|
80
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.inject_uid(meta, content, existing_uid)
|
|
84
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
85
|
-
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
86
|
-
[m, content]
|
|
87
|
-
end
|
|
88
|
-
|
|
89
62
|
def self.validate_path_extension(path, nested)
|
|
90
63
|
ext = File.extname(path)
|
|
91
64
|
if nested
|
|
@@ -98,6 +71,11 @@ module Textus
|
|
|
98
71
|
|
|
99
72
|
raise UsageError.new("json format requires '.json' path (got #{ext.inspect})")
|
|
100
73
|
end
|
|
74
|
+
|
|
75
|
+
def self.data_to_payload(data)
|
|
76
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
77
|
+
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
78
|
+
end
|
|
101
79
|
end
|
|
102
80
|
end
|
|
103
81
|
end
|
|
@@ -43,39 +43,17 @@ module Textus
|
|
|
43
43
|
[bytes, meta, body.to_s, nil]
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
47
|
-
raw = File.binread(path)
|
|
48
|
-
parsed = parse(raw, path: path)
|
|
49
|
-
meta = parsed["_meta"] || {}
|
|
50
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
51
|
-
|
|
52
|
-
new_meta = meta.merge("name" => basename)
|
|
53
|
-
File.binwrite(path, serialize(meta: new_meta, body: parsed["body"]))
|
|
54
|
-
true
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.enforce_name_match!(path, meta)
|
|
58
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
59
|
-
|
|
60
|
-
ext = extensions.first
|
|
61
|
-
basename = File.basename(path, ext)
|
|
62
|
-
return if meta["name"] == basename
|
|
63
|
-
|
|
64
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.inject_uid(meta, content, existing_uid)
|
|
68
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
69
|
-
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
70
|
-
[m, content]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
46
|
def self.validate_path_extension(path, _nested)
|
|
74
47
|
ext = File.extname(path)
|
|
75
48
|
return if ["", ".md"].include?(ext)
|
|
76
49
|
|
|
77
50
|
raise UsageError.new("markdown format requires '.md' path (got #{ext.inspect})")
|
|
78
51
|
end
|
|
52
|
+
|
|
53
|
+
def self.data_to_payload(data)
|
|
54
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
55
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
56
|
+
end
|
|
79
57
|
end
|
|
80
58
|
end
|
|
81
59
|
end
|
data/lib/textus/format/text.rb
CHANGED
|
@@ -20,10 +20,6 @@ module Textus
|
|
|
20
20
|
|
|
21
21
|
def self.nested_glob = "**/*.txt"
|
|
22
22
|
|
|
23
|
-
def self.inject_uid(meta, content, _existing_uid)
|
|
24
|
-
[meta, content]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
23
|
def self.enforce_name_match!(_path, _meta); end
|
|
28
24
|
|
|
29
25
|
def self.serialize_for_put(meta:, body:, content:, path:)
|
|
@@ -49,6 +45,11 @@ module Textus
|
|
|
49
45
|
|
|
50
46
|
raise UsageError.new("text format requires '.txt' or no extension (got #{ext.inspect})")
|
|
51
47
|
end
|
|
48
|
+
|
|
49
|
+
def self.data_to_payload(data)
|
|
50
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
51
|
+
{ meta: data["_meta"] || {}, body: (data["body"] || "").to_s, content: nil }
|
|
52
|
+
end
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
end
|
data/lib/textus/format/yaml.rb
CHANGED
|
@@ -37,6 +37,31 @@ module Textus
|
|
|
37
37
|
schema.validate!(parsed["content"] || {})
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
RAW_REQUIRED = %w[ingested_at content_hash].freeze
|
|
41
|
+
RAW_SOURCE_KINDS = %w[url file asset].freeze
|
|
42
|
+
|
|
43
|
+
def self.validate_raw_entry!(parsed, lane)
|
|
44
|
+
return unless lane == "raw"
|
|
45
|
+
|
|
46
|
+
content = parsed["content"] || {}
|
|
47
|
+
return if content["superseded_by"]
|
|
48
|
+
|
|
49
|
+
missing = RAW_REQUIRED.reject { |f| content[f] }
|
|
50
|
+
raise Textus::BadContent.new(nil, "raw entry missing required field(s): #{missing.join(", ")}") if missing.any?
|
|
51
|
+
|
|
52
|
+
source = content["source"] || {}
|
|
53
|
+
kind = source["kind"]
|
|
54
|
+
unless RAW_SOURCE_KINDS.include?(kind)
|
|
55
|
+
raise Textus::BadContent.new(
|
|
56
|
+
nil, "raw entry source.kind must be #{RAW_SOURCE_KINDS.join("|")}, got #{kind.inspect}"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return unless kind == "url" && !source["url"]
|
|
61
|
+
|
|
62
|
+
raise Textus::BadContent.new(nil, "raw entry with source.kind=url must have source.url")
|
|
63
|
+
end
|
|
64
|
+
|
|
40
65
|
def self.extensions = [".yaml", ".yml"]
|
|
41
66
|
|
|
42
67
|
def self.nested_glob = "**/*.{yaml,yml}"
|
|
@@ -57,33 +82,6 @@ module Textus
|
|
|
57
82
|
end
|
|
58
83
|
end
|
|
59
84
|
|
|
60
|
-
def self.rewrite_name(path, basename) # rubocop:disable Naming/PredicateMethod
|
|
61
|
-
raw = File.binread(path)
|
|
62
|
-
parsed = parse(raw, path: path)
|
|
63
|
-
meta = parsed["_meta"]
|
|
64
|
-
return false unless meta.is_a?(Hash) && meta["name"].is_a?(String) && meta["name"] != basename
|
|
65
|
-
|
|
66
|
-
new_meta = meta.merge("name" => basename)
|
|
67
|
-
File.binwrite(path, serialize(meta: new_meta, body: "", content: parsed["content"]))
|
|
68
|
-
true
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.enforce_name_match!(path, meta)
|
|
72
|
-
return unless meta.is_a?(Hash) && meta["name"]
|
|
73
|
-
|
|
74
|
-
ext = extensions.first
|
|
75
|
-
basename = File.basename(path, ext)
|
|
76
|
-
return if meta["name"] == basename
|
|
77
|
-
|
|
78
|
-
raise BadFrontmatter.new(path, "name '#{meta["name"]}' does not match basename '#{basename}'")
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def self.inject_uid(meta, content, existing_uid)
|
|
82
|
-
m = meta.is_a?(Hash) ? meta.dup : {}
|
|
83
|
-
m["uid"] = existing_uid || Textus::Uid.mint unless m["uid"].is_a?(String) && !m["uid"].empty?
|
|
84
|
-
[m, content]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
85
|
def self.validate_path_extension(path, nested)
|
|
88
86
|
ext = File.extname(path)
|
|
89
87
|
if nested
|
|
@@ -96,6 +94,11 @@ module Textus
|
|
|
96
94
|
|
|
97
95
|
raise UsageError.new("yaml format requires '.yaml' or '.yml' path (got #{ext.inspect})")
|
|
98
96
|
end
|
|
97
|
+
|
|
98
|
+
def self.data_to_payload(data)
|
|
99
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
100
|
+
{ meta: data["_meta"] || {}, body: nil, content: data["content"] || data }
|
|
101
|
+
end
|
|
99
102
|
end
|
|
100
103
|
end
|
|
101
104
|
end
|
data/lib/textus/format.rb
CHANGED
|
@@ -9,6 +9,18 @@ module Textus
|
|
|
9
9
|
"text" => -> { Format::Text },
|
|
10
10
|
}.freeze
|
|
11
11
|
|
|
12
|
+
# Optional registry for injectable format strategies. Tests or app
|
|
13
|
+
# initializers can set Format.registry = { "custom" => -> { MyFormat }}
|
|
14
|
+
@registry = nil
|
|
15
|
+
|
|
16
|
+
def self.registry=(reg)
|
|
17
|
+
@registry = reg
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.registry
|
|
21
|
+
@registry
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
EXT_TO_FORMAT = {
|
|
13
25
|
".md" => "markdown",
|
|
14
26
|
".json" => "json",
|
|
@@ -18,7 +30,10 @@ module Textus
|
|
|
18
30
|
}.freeze
|
|
19
31
|
|
|
20
32
|
def self.for(format)
|
|
21
|
-
|
|
33
|
+
key = format.to_s
|
|
34
|
+
return registry.fetch(key).call if registry&.key?(key)
|
|
35
|
+
|
|
36
|
+
STRATEGIES.fetch(key) { raise Textus::UsageError.new("unknown entry format: #{format.inspect}") }.call
|
|
22
37
|
end
|
|
23
38
|
|
|
24
39
|
def self.infer_from_extension(ext)
|
|
@@ -36,5 +51,11 @@ module Textus
|
|
|
36
51
|
def self.serialize(meta: {}, body: "", content: nil, format: "markdown")
|
|
37
52
|
Format.for(format).serialize(meta: meta, body: body, content: content)
|
|
38
53
|
end
|
|
54
|
+
|
|
55
|
+
def self.data_to_payload(data, format)
|
|
56
|
+
return { meta: {}, body: "", content: nil } if data.nil?
|
|
57
|
+
|
|
58
|
+
Format.for(format).data_to_payload(data)
|
|
59
|
+
end
|
|
39
60
|
end
|
|
40
61
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class BootStore
|
|
5
|
+
def initialize(container:)
|
|
6
|
+
@container = container
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(_command, _call)
|
|
10
|
+
Value::Result.success(Textus::Boot.build(container: @container))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class DoctorStore
|
|
5
|
+
def initialize(container:)
|
|
6
|
+
@container = container
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(command, call)
|
|
10
|
+
Value::Result.success(Textus::Doctor.build(container: @container, checks: command.checks, role: call.role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Handlers
|
|
3
|
+
module Maintenance
|
|
4
|
+
class DrainStore
|
|
5
|
+
def initialize(container:, job_store:)
|
|
6
|
+
@container = container
|
|
7
|
+
@job_store = job_store
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(_command, call)
|
|
11
|
+
queue = Textus::Store::Jobs::Queue.new(store: @job_store)
|
|
12
|
+
Textus::Store::Jobs::Planner.seed(container: @container, queue: queue, role: call.role)
|
|
13
|
+
queue.reclaim(now: Textus::Port::Clock.new.now)
|
|
14
|
+
summary = Textus::Store::Jobs::Worker.for(container: @container, queue: queue).drain
|
|
15
|
+
Value::Result.success("protocol" => Textus::PROTOCOL, "ok" => summary.failed.zero?,
|
|
16
|
+
"completed" => summary.completed, "failed" => summary.failed)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "date"
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Handlers
|
|
7
|
+
module Maintenance
|
|
8
|
+
class IngestEntry
|
|
9
|
+
SOURCE_KINDS = %w[url file asset].freeze
|
|
10
|
+
CONTENT_HASH_ALGO = "sha256"
|
|
11
|
+
|
|
12
|
+
def initialize(container:)
|
|
13
|
+
@container = container
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(command, call)
|
|
17
|
+
unless SOURCE_KINDS.include?(command.kind)
|
|
18
|
+
return Value::Result.failure(:usage_error,
|
|
19
|
+
"ingest kind must be one of #{SOURCE_KINDS.join("|")}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
case command.kind
|
|
23
|
+
when "url" then return Value::Result.failure(:usage_error, "ingest url requires url") unless command.url
|
|
24
|
+
when "file" then return Value::Result.failure(:usage_error, "ingest file requires path") unless command.path
|
|
25
|
+
when "asset"
|
|
26
|
+
return Value::Result.failure(:usage_error, "ingest asset requires path") unless command.path
|
|
27
|
+
return Value::Result.failure(:usage_error, "ingest asset requires lane") unless command.lane
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
now = Time.now.utc
|
|
31
|
+
key = derive_key(now, command.kind, command.slug)
|
|
32
|
+
content_hash = compute_content_hash(command)
|
|
33
|
+
mentry = @container.manifest.resolver.resolve(key).entry
|
|
34
|
+
ts = now.iso8601
|
|
35
|
+
|
|
36
|
+
structured = build_structured(ts, now, content_hash, command)
|
|
37
|
+
store = @container.job_store
|
|
38
|
+
index = Textus::Store::Index::Lookup.new(store:)
|
|
39
|
+
|
|
40
|
+
duplicate_key = find_duplicate(index, content_hash, command)
|
|
41
|
+
|
|
42
|
+
env = if duplicate_key && duplicate_key != key
|
|
43
|
+
supersede_entry(duplicate_key, key, structured, call, store, command)
|
|
44
|
+
else
|
|
45
|
+
write_entry(key, structured, mentry, call)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
rebuild_index(store)
|
|
49
|
+
Value::Result.success(env)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def derive_key(now, kind, slug)
|
|
55
|
+
date = now.strftime("%Y.%m.%d")
|
|
56
|
+
"raw.#{date}.#{kind}-#{slug}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def compute_content_hash(command)
|
|
60
|
+
digest = Digest::SHA256.new
|
|
61
|
+
case command.kind
|
|
62
|
+
when "url" then digest.update(command.url)
|
|
63
|
+
when "file", "asset" then digest.file(command.path)
|
|
64
|
+
end
|
|
65
|
+
"#{CONTENT_HASH_ALGO}:#{digest.hexdigest}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_structured(timestamp, now, content_hash, command)
|
|
69
|
+
base = { "ingested_at" => timestamp, "content_hash" => content_hash }
|
|
70
|
+
case command.kind
|
|
71
|
+
when "url"
|
|
72
|
+
base.merge("source" => { "kind" => "url", "url" => command.url,
|
|
73
|
+
"label" => command.label || command.url }, "body" => nil)
|
|
74
|
+
when "file"
|
|
75
|
+
base.merge("source" => { "kind" => "file", "path" => command.path,
|
|
76
|
+
"label" => command.label || File.basename(command.path) },
|
|
77
|
+
"body" => File.read(command.path))
|
|
78
|
+
when "asset"
|
|
79
|
+
asset_rel = copy_asset(now, command.path, command.lane)
|
|
80
|
+
base.merge("source" => { "kind" => "asset",
|
|
81
|
+
"label" => command.label || File.basename(command.path) },
|
|
82
|
+
"asset" => asset_rel, "body" => nil)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def copy_asset(now, path, lane)
|
|
87
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
88
|
+
filename = File.basename(path)
|
|
89
|
+
assets_dir = @container.layout.asset_raw_dir(date_path, lane)
|
|
90
|
+
FileUtils.mkdir_p(assets_dir)
|
|
91
|
+
FileUtils.cp(path, File.join(assets_dir, filename))
|
|
92
|
+
sentinel = @container.layout.asset_sentinel_path
|
|
93
|
+
File.write(sentinel, "*\n") unless File.exist?(sentinel)
|
|
94
|
+
"raw/#{date_path}/#{lane}/#{filename}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write_entry(key, structured, mentry, call)
|
|
98
|
+
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
99
|
+
writer.put(key, mentry: mentry,
|
|
100
|
+
payload: Textus::Value::Payload.new(meta: nil, body: nil, content: structured))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_duplicate(index, content_hash, command)
|
|
104
|
+
dup = index.find_by_hash(content_hash)
|
|
105
|
+
return dup if dup
|
|
106
|
+
return unless command.kind == "url"
|
|
107
|
+
|
|
108
|
+
index.find_by_url(command.url)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def supersede_entry(old_key, new_key, structured, call, store, command)
|
|
112
|
+
old_mentry = @container.manifest.resolver.resolve(old_key).entry
|
|
113
|
+
reader = Store::Entry::Reader.from(container: @container)
|
|
114
|
+
old_env = reader.read(old_key)
|
|
115
|
+
old_content = old_env&.content || {}
|
|
116
|
+
tombstone = {}
|
|
117
|
+
%w[ingested_at].each { |k| tombstone[k] = old_content[k] if old_content.key?(k) }
|
|
118
|
+
source_kind = old_content.dig("source", "kind")
|
|
119
|
+
tombstone["source"] = { "kind" => source_kind } if source_kind
|
|
120
|
+
tombstone["superseded_by"] = new_key
|
|
121
|
+
|
|
122
|
+
writer = Store::Entry::Writer.from(container: @container, call: call)
|
|
123
|
+
writer.put(old_key, mentry: old_mentry,
|
|
124
|
+
payload: Textus::Value::Payload.new(meta: nil, body: nil, content: tombstone))
|
|
125
|
+
|
|
126
|
+
structured["supersedes"] = old_key
|
|
127
|
+
env = write_entry(new_key, structured,
|
|
128
|
+
@container.manifest.resolver.resolve(new_key).entry, call)
|
|
129
|
+
|
|
130
|
+
move_asset(old_content["asset"], command.lane) if command.kind == "asset" && old_content["asset"]
|
|
131
|
+
|
|
132
|
+
rebuild_index(store)
|
|
133
|
+
env
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def move_asset(old_rel, lane)
|
|
137
|
+
old_path = @container.layout.asset_resolve(old_rel)
|
|
138
|
+
return unless File.exist?(old_path)
|
|
139
|
+
|
|
140
|
+
now = Time.now.utc
|
|
141
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
142
|
+
filename = File.basename(old_path)
|
|
143
|
+
new_dir = @container.layout.asset_raw_dir(date_path, lane)
|
|
144
|
+
new_path = File.join(new_dir, filename)
|
|
145
|
+
return if old_path == new_path
|
|
146
|
+
|
|
147
|
+
FileUtils.mkdir_p(new_dir)
|
|
148
|
+
FileUtils.mv(old_path, new_path)
|
|
149
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
150
|
+
warn "[textus ingest] could not move asset #{old_rel}: #{e.message}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def rebuild_index(store)
|
|
154
|
+
Textus::Store::Index::Builder.new(store:).rebuild!(resolver: @container.manifest.resolver)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|